From 67e8d63ddbcfd981884ef9c1552409e24a416f0a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 17 Dec 2019 09:25:21 +0100 Subject: [PATCH] Migrate instance uuid logic to new platform (#52060) * move and migrate uuid code to new platform * create and wire uuid service * handle legacy compatibility * update generated docs * add `set` to LegacyConfig interface * Fix types * fix config access * respect naming conventions for uuid * remove hardcoded config paths * rename manageInstanceUuid to resolveInstanceUuid * moves legacy config uuid set from uuid to legacy service * log specific message depending on how uuid was resolved * resolve merge conflicts * use fs.promises * add forgotten @public in uuid contract * add explicit errors and tests * ensure uuid is valid in configuration * fix read/write tests --- .../server/kibana-plugin-server.coresetup.md | 1 + .../kibana-plugin-server.coresetup.uuid.md | 13 ++ .../core/server/kibana-plugin-server.md | 1 + ...server.uuidservicesetup.getinstanceuuid.md | 17 ++ .../kibana-plugin-server.uuidservicesetup.md | 20 ++ src/core/server/http/http_config.test.ts | 9 + src/core/server/http/http_config.ts | 6 + src/core/server/index.ts | 4 + src/core/server/internal_types.ts | 2 + ...gacy_object_to_config_adapter.test.ts.snap | 2 + .../config/legacy_object_to_config_adapter.ts | 1 + src/core/server/legacy/config/types.ts | 2 + src/core/server/legacy/legacy_service.mock.ts | 39 ++++ .../legacy/legacy_service.test.mocks.ts | 34 +++ src/core/server/legacy/legacy_service.test.ts | 81 +++++-- src/core/server/legacy/legacy_service.ts | 6 + src/core/server/mocks.ts | 3 + src/core/server/plugins/plugin_context.ts | 3 + .../migrations/core/build_index_map.test.ts | 10 +- src/core/server/server.api.md | 7 + src/core/server/server.test.mocks.ts | 6 + src/core/server/server.ts | 6 + src/core/server/uuid/index.ts | 20 ++ src/core/server/uuid/resolve_uuid.test.ts | 210 ++++++++++++++++++ src/core/server/uuid/resolve_uuid.ts | 109 +++++++++ src/core/server/uuid/uuid_service.mock.ts | 41 ++++ src/core/server/uuid/uuid_service.test.ts | 58 +++++ src/core/server/uuid/uuid_service.ts | 55 +++++ src/legacy/core_plugins/kibana/index.js | 3 - .../server/lib/__tests__/manage_uuid.js | 102 --------- .../kibana/server/lib/manage_uuid.js | 97 -------- src/legacy/server/config/schema.js | 4 +- src/legacy/server/kbn_server.d.ts | 3 +- 33 files changed, 742 insertions(+), 233 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.coresetup.uuid.md create mode 100644 docs/development/core/server/kibana-plugin-server.uuidservicesetup.getinstanceuuid.md create mode 100644 docs/development/core/server/kibana-plugin-server.uuidservicesetup.md create mode 100644 src/core/server/legacy/legacy_service.mock.ts create mode 100644 src/core/server/legacy/legacy_service.test.mocks.ts create mode 100644 src/core/server/uuid/index.ts create mode 100644 src/core/server/uuid/resolve_uuid.test.ts create mode 100644 src/core/server/uuid/resolve_uuid.ts create mode 100644 src/core/server/uuid/uuid_service.mock.ts create mode 100644 src/core/server/uuid/uuid_service.test.ts create mode 100644 src/core/server/uuid/uuid_service.ts delete mode 100644 src/legacy/core_plugins/kibana/server/lib/__tests__/manage_uuid.js delete mode 100644 src/legacy/core_plugins/kibana/server/lib/manage_uuid.js diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 3886b6e05e657..3f7f5b727ee80 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -22,4 +22,5 @@ export interface CoreSetup | [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | | [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | +| [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.uuid.md b/docs/development/core/server/kibana-plugin-server.coresetup.uuid.md new file mode 100644 index 0000000000000..2b9077735d8e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.uuid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [uuid](./kibana-plugin-server.coresetup.uuid.md) + +## CoreSetup.uuid property + +[UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) + +Signature: + +```typescript +uuid: UuidServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index e97ecbcfaf739..ea5ca6502b076 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -138,6 +138,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | | | [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) | Describes the values explicitly set by user. | +| [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | APIs to access the application's instance uuid. | ## Variables diff --git a/docs/development/core/server/kibana-plugin-server.uuidservicesetup.getinstanceuuid.md b/docs/development/core/server/kibana-plugin-server.uuidservicesetup.getinstanceuuid.md new file mode 100644 index 0000000000000..e0b7012bea4aa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uuidservicesetup.getinstanceuuid.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) > [getInstanceUuid](./kibana-plugin-server.uuidservicesetup.getinstanceuuid.md) + +## UuidServiceSetup.getInstanceUuid() method + +Retrieve the Kibana instance uuid. + +Signature: + +```typescript +getInstanceUuid(): string; +``` +Returns: + +`string` + diff --git a/docs/development/core/server/kibana-plugin-server.uuidservicesetup.md b/docs/development/core/server/kibana-plugin-server.uuidservicesetup.md new file mode 100644 index 0000000000000..f2a6cfdeac704 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uuidservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) + +## UuidServiceSetup interface + +APIs to access the application's instance uuid. + +Signature: + +```typescript +export interface UuidServiceSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [getInstanceUuid()](./kibana-plugin-server.uuidservicesetup.getinstanceuuid.md) | Retrieve the Kibana instance uuid. | + diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 888313e1478cb..9b6fab8f3daec 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import uuid from 'uuid'; import { config, HttpConfig } from '.'; import { Env } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; @@ -77,6 +78,14 @@ test('throws if basepath is not specified, but rewriteBasePath is set', () => { expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); +test('accepts only valid uuids for server.uuid', () => { + const httpSchema = config.schema; + expect(() => httpSchema.validate({ uuid: uuid.v4() })).not.toThrow(); + expect(() => httpSchema.validate({ uuid: 'not an uuid' })).toThrowErrorMatchingInlineSnapshot( + `"[uuid]: must be a valid uuid"` + ); +}); + describe('with TLS', () => { test('throws if TLS is enabled but `key` is not specified', () => { const httpSchema = config.schema; diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 912459c83df6e..ef6a9c0a5f1a5 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -23,6 +23,7 @@ import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /(^$|^\/.*[^\/]$)/; +const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; @@ -92,6 +93,11 @@ export const config = { ) ), }), + uuid: schema.maybe( + schema.string({ + validate: match(uuidRegexp, 'must be a valid uuid'), + }) + ), }, { validate: rawConfig => { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 835c5872d51a3..2aaa8306e871f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -47,6 +47,7 @@ import { IUiSettingsClient, UiSettingsServiceSetup } from './ui_settings'; import { SavedObjectsClientContract } from './saved_objects/types'; import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; +import { UuidServiceSetup } from './uuid'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; @@ -269,6 +270,8 @@ export interface CoreSetup { savedObjects: SavedObjectsServiceSetup; /** {@link UiSettingsServiceSetup} */ uiSettings: UiSettingsServiceSetup; + /** {@link UuidServiceSetup} */ + uuid: UuidServiceSetup; } /** @@ -290,4 +293,5 @@ export { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId, + UuidServiceSetup, }; diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 07fd77f83d774..06cf848bff25a 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -26,6 +26,7 @@ import { InternalSavedObjectsServiceSetup, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; +import { UuidServiceSetup } from './uuid'; /** @internal */ export interface InternalCoreSetup { @@ -35,6 +36,7 @@ export interface InternalCoreSetup { elasticsearch: InternalElasticsearchServiceSetup; uiSettings: InternalUiSettingsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; + uuid: UuidServiceSetup; } /** diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index d327860052eb9..0ebd8b8371628 100644 --- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -20,6 +20,7 @@ Object { "keyPassphrase": "some-phrase", "someNewValue": "new", }, + "uuid": undefined, } `; @@ -43,6 +44,7 @@ Object { "enabled": false, "key": "key", }, + "uuid": undefined, } `; diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index 75e1813f8c1f6..ffcbfda4e024d 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -73,6 +73,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { keepaliveTimeout: configValue.keepaliveTimeout, socketTimeout: configValue.socketTimeout, compression: configValue.compression, + uuid: configValue.uuid, }; } diff --git a/src/core/server/legacy/config/types.ts b/src/core/server/legacy/config/types.ts index 24869e361c39c..cac1002d6c244 100644 --- a/src/core/server/legacy/config/types.ts +++ b/src/core/server/legacy/config/types.ts @@ -25,6 +25,8 @@ export interface LegacyConfig { get(key?: string): T; has(key: string): boolean; + set(key: string, value: any): void; + set(config: Record): void; } /** diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts new file mode 100644 index 0000000000000..ac0319cdf4eb5 --- /dev/null +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -0,0 +1,39 @@ +/* + * 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 { LegacyServiceDiscoverPlugins } from './legacy_service'; + +const createDiscoverMock = () => { + const setupContract: DeeplyMockedKeys = { + pluginSpecs: [], + disabledPluginSpecs: [], + uiExports: {} as any, + settings: {}, + pluginExtendedConfig: { + get: jest.fn(), + has: jest.fn(), + set: jest.fn(), + } as any, + }; + return setupContract; +}; + +export const legacyServiceMock = { + createDiscover: createDiscoverMock, +}; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts new file mode 100644 index 0000000000000..e8d4a0ed0bd4d --- /dev/null +++ b/src/core/server/legacy/legacy_service.test.mocks.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const findLegacyPluginSpecsMock = jest + .fn() + .mockImplementation((settings: Record) => ({ + pluginSpecs: [], + pluginExtendedConfig: { + has: jest.fn(), + get: jest.fn(() => settings), + set: jest.fn(), + }, + disabledPluginSpecs: [], + uiExports: [], + })); +jest.doMock('./plugins/find_legacy_plugin_specs.ts', () => ({ + findLegacyPluginSpecs: findLegacyPluginSpecsMock, +})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d4360c577d24c..17ec1e9756432 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -17,36 +17,34 @@ * under the License. */ -import { BehaviorSubject, throwError } from 'rxjs'; - jest.mock('../../../legacy/server/kbn_server'); jest.mock('../../../cli/cluster/cluster_manager'); -jest.mock('./plugins/find_legacy_plugin_specs'); jest.mock('./config/legacy_deprecation_adapters', () => ({ convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider), })); +import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; +import { BehaviorSubject, throwError } from 'rxjs'; import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; // @ts-ignore: implicit any for JS file import MockClusterManager from '../../../cli/cluster/cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; -import { contextServiceMock } from '../context/context_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; -import { configServiceMock } from '../config/config_service.mock'; - import { BasePathProxyServer } from '../http'; -import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin } from '../plugins'; +import { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs'; +import { configServiceMock } from '../config/config_service.mock'; +import { loggingServiceMock } from '../logging/logging_service.mock'; +import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; -import { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs'; +import { uuidServiceMock } from '../uuid/uuid_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; -const findLegacyPluginSpecsMock: jest.Mock = findLegacyPluginSpecs as any; let coreId: symbol; let env: Env; @@ -58,23 +56,17 @@ let startDeps: LegacyServiceStartDeps; const logger = loggingServiceMock.create(); let configService: ReturnType; +let uuidSetup: ReturnType; beforeEach(() => { coreId = Symbol(); env = Env.createDefault(getEnvOptions()); configService = configServiceMock.create(); + uuidSetup = uuidServiceMock.createSetupContract(); - findLegacyPluginSpecsMock.mockImplementation( - settings => - Promise.resolve({ - pluginSpecs: [], - pluginExtendedConfig: settings, - disabledPluginSpecs: [], - uiExports: [], - }) as any - ); - + findLegacyPluginSpecsMock.mockClear(); MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); + MockKbnServer.prototype.listen = jest.fn(); setupDeps = { core: { @@ -97,6 +89,7 @@ beforeEach(() => { browserConfigs: new Map(), }, }, + uuid: uuidSetup, }, plugins: { 'plugin-id': 'plugin-value' }, }; @@ -123,7 +116,6 @@ beforeEach(() => { afterEach(() => { jest.clearAllMocks(); - findLegacyPluginSpecsMock.mockReset(); }); describe('once LegacyService is set up with connection info', () => { @@ -142,11 +134,15 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: true }, server: { autoListen: true } }, { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value + expect.objectContaining({ get: expect.any(Function) }), expect.any(Object), { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ + path: { autoListen: true }, + server: { autoListen: true }, + }); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); @@ -169,10 +165,14 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( { path: { autoListen: false }, server: { autoListen: true } }, - { path: { autoListen: false }, server: { autoListen: true } }, + expect.objectContaining({ get: expect.any(Function) }), expect.any(Object), { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ + path: { autoListen: false }, + server: { autoListen: true }, + }); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); @@ -306,10 +306,14 @@ describe('once LegacyService is set up without connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( { path: {}, server: { autoListen: true } }, - { path: {}, server: { autoListen: true } }, + expect.objectContaining({ get: expect.any(Function) }), expect.any(Object), { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ + path: {}, + server: { autoListen: true }, + }); }); test('reconfigures logging configuration if new config is received.', async () => { @@ -440,3 +444,34 @@ describe('#discoverPlugins()', () => { expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB'); }); }); + +test('Sets the server.uuid property on the legacy configuration', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + uuidSetup.getInstanceUuid.mockImplementation(() => 'UUID_FROM_SERVICE'); + + const configSetMock = jest.fn(); + + findLegacyPluginSpecsMock.mockImplementation((settings: Record) => ({ + pluginSpecs: [], + pluginExtendedConfig: { + has: jest.fn(), + get: jest.fn(() => settings), + set: configSetMock, + }, + disabledPluginSpecs: [], + uiExports: [], + })); + + await legacyService.discoverPlugins(); + await legacyService.setup(setupDeps); + + expect(configSetMock).toHaveBeenCalledTimes(1); + expect(configSetMock).toHaveBeenCalledWith('server.uuid', 'UUID_FROM_SERVICE'); +}); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 662cc0bdf2f3a..1bba38433d7f4 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -200,6 +200,9 @@ export class LegacyService implements CoreService { 'Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()' ); } + // propagate the instance uuid to the legacy config, as it was the legacy way to access it. + this.legacyRawConfig.set('server.uuid', setupDeps.core.uuid.getInstanceUuid()); + this.setupDeps = setupDeps; } @@ -300,6 +303,9 @@ export class LegacyService implements CoreService { uiSettings: { register: setupDeps.core.uiSettings.register, }, + uuid: { + getInstanceUuid: setupDeps.core.uuid.getInstanceUuid, + }, }; const coreStart: CoreStart = { capabilities: startDeps.core.capabilities, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 07b60e771d643..3a68b18409b0a 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -38,6 +38,7 @@ export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +import { uuidServiceMock } from './uuid/uuid_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -110,6 +111,7 @@ function createCoreSetupMock() { http: httpMock, savedObjects: savedObjectsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, + uuid: uuidServiceMock.createSetupContract(), }; return mock; @@ -132,6 +134,7 @@ function createInternalCoreSetupMock() { http: httpServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createSetupContract(), + uuid: uuidServiceMock.createSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 26c65baf95535..04a7547fd3747 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -173,6 +173,9 @@ export function createPluginSetupContext( uiSettings: { register: deps.uiSettings.register, }, + uuid: { + getInstanceUuid: deps.uuid.getInstanceUuid, + }, }; } diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts index 622f32d3e653b..4fcaf50d4f538 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts @@ -22,9 +22,11 @@ import { ObjectToConfigAdapter } from '../../../config'; import { SavedObjectsSchema } from '../../schema'; import { LegacyConfig } from '../../../legacy/config'; +const config = (new ObjectToConfigAdapter({}) as unknown) as LegacyConfig; + test('mappings without index pattern goes to default index', () => { const result = createIndexMap({ - config: new ObjectToConfigAdapter({}) as LegacyConfig, + config, kibanaIndexName: '.kibana', schema: new SavedObjectsSchema({ type1: { @@ -58,7 +60,7 @@ test('mappings without index pattern goes to default index', () => { test(`mappings with custom index pattern doesn't go to default index`, () => { const result = createIndexMap({ - config: new ObjectToConfigAdapter({}) as LegacyConfig, + config, kibanaIndexName: '.kibana', schema: new SavedObjectsSchema({ type1: { @@ -93,7 +95,7 @@ test(`mappings with custom index pattern doesn't go to default index`, () => { test('creating a script gets added to the index pattern', () => { const result = createIndexMap({ - config: new ObjectToConfigAdapter({}) as LegacyConfig, + config, kibanaIndexName: '.kibana', schema: new SavedObjectsSchema({ type1: { @@ -158,7 +160,7 @@ test('throws when two scripts are defined for an index pattern', () => { }; expect(() => createIndexMap({ - config: new ObjectToConfigAdapter({}) as LegacyConfig, + config, kibanaIndexName: defaultIndex, schema, indexMap, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 780a1532a859e..4e6493a17aea1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -564,6 +564,8 @@ export interface CoreSetup { savedObjects: SavedObjectsServiceSetup; // (undocumented) uiSettings: UiSettingsServiceSetup; + // (undocumented) + uuid: UuidServiceSetup; } // @public @@ -1831,6 +1833,11 @@ export interface UserProvidedValues { userValue?: T; } +// @public +export interface UuidServiceSetup { + getInstanceUuid(): string; +} + // @public export const validBodyOutput: readonly ["data", "stream"]; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 7820a14fec8f3..59925f46543e7 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -69,3 +69,9 @@ export const mockEnsureValidConfiguration = jest.fn(); jest.doMock('./legacy/config/ensure_valid_configuration', () => ({ ensureValidConfiguration: mockEnsureValidConfiguration, })); + +import { uuidServiceMock } from './uuid/uuid_service.mock'; +export const mockUuidService = uuidServiceMock.create(); +jest.doMock('./uuid/uuid_service', () => ({ + UuidService: jest.fn(() => mockUuidService), +})); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 725a45f131992..89d99d6c4a4ec 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -49,6 +49,7 @@ import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup } from './internal_types'; import { CapabilitiesService } from './capabilities'; +import { UuidService } from './uuid'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -64,6 +65,7 @@ export class Server { private readonly plugins: PluginsService; private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; + private readonly uuid: UuidService; constructor( rawConfigProvider: RawConfigurationProvider, @@ -82,6 +84,7 @@ export class Server { this.savedObjects = new SavedObjectsService(core); this.uiSettings = new UiSettingsService(core); this.capabilities = new CapabilitiesService(core); + this.uuid = new UuidService(core); } public async setup() { @@ -106,6 +109,8 @@ export class Server { ]), }); + const uuidSetup = await this.uuid.setup(); + const httpSetup = await this.http.setup({ context: contextServiceSetup, }); @@ -134,6 +139,7 @@ export class Server { http: httpSetup, uiSettings: uiSettingsSetup, savedObjects: savedObjectsSetup, + uuid: uuidSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); diff --git a/src/core/server/uuid/index.ts b/src/core/server/uuid/index.ts new file mode 100644 index 0000000000000..ad57041a124c9 --- /dev/null +++ b/src/core/server/uuid/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { UuidService, UuidServiceSetup } from './uuid_service'; diff --git a/src/core/server/uuid/resolve_uuid.test.ts b/src/core/server/uuid/resolve_uuid.test.ts new file mode 100644 index 0000000000000..1ddd667eacdee --- /dev/null +++ b/src/core/server/uuid/resolve_uuid.test.ts @@ -0,0 +1,210 @@ +/* + * 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 { promises } from 'fs'; +import { join } from 'path'; +import { resolveInstanceUuid } from './resolve_uuid'; +import { configServiceMock } from '../config/config_service.mock'; +import { loggingServiceMock } from '../logging/logging_service.mock'; +import { BehaviorSubject } from 'rxjs'; +import { Logger } from '../logging'; + +const { readFile, writeFile } = promises; + +jest.mock('uuid', () => ({ + v4: () => 'NEW_UUID', +})); + +jest.mock('fs', () => { + const actual = jest.requireActual('fs'); + return { + ...actual, + promises: { + ...actual.promises, + readFile: jest.fn(() => Promise.resolve('')), + writeFile: jest.fn(() => Promise.resolve('')), + }, + }; +}); + +const DEFAULT_FILE_UUID = 'FILE_UUID'; +const DEFAULT_CONFIG_UUID = 'CONFIG_UUID'; +const fileNotFoundError = { code: 'ENOENT' }; +const permissionError = { code: 'EACCES' }; +const isDirectoryError = { code: 'EISDIR' }; + +const mockReadFile = ({ + uuid = DEFAULT_FILE_UUID, + error = null, +}: Partial<{ + uuid: string; + error: any; +}>) => { + ((readFile as unknown) as jest.Mock).mockImplementation(() => { + if (error) { + return Promise.reject(error); + } else { + return Promise.resolve(uuid); + } + }); +}; + +const mockWriteFile = (error?: object) => { + ((writeFile as unknown) as jest.Mock).mockImplementation(() => { + if (error) { + return Promise.reject(error); + } else { + return Promise.resolve(); + } + }); +}; + +const getConfigService = (serverUuid: string | undefined) => { + const configService = configServiceMock.create(); + configService.atPath.mockImplementation(path => { + if (path === 'path') { + return new BehaviorSubject({ + data: 'data-folder', + }); + } + if (path === 'server') { + return new BehaviorSubject({ + uuid: serverUuid, + }); + } + return new BehaviorSubject({}); + }); + return configService; +}; + +describe('resolveInstanceUuid', () => { + let configService: ReturnType; + let logger: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + mockReadFile({ uuid: DEFAULT_FILE_UUID }); + mockWriteFile(); + configService = getConfigService(DEFAULT_CONFIG_UUID); + logger = loggingServiceMock.create().get() as any; + }); + + describe('when file is present and config property is set', () => { + it('writes to file and returns the config uuid if they mismatch', async () => { + const uuid = await resolveInstanceUuid(configService, logger); + expect(uuid).toEqual(DEFAULT_CONFIG_UUID); + expect(writeFile).toHaveBeenCalledWith( + join('data-folder', 'uuid'), + DEFAULT_CONFIG_UUID, + expect.any(Object) + ); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Updating Kibana instance UUID to: CONFIG_UUID (was: FILE_UUID)", + ] + `); + }); + it('does not write to file if they match', async () => { + mockReadFile({ uuid: DEFAULT_CONFIG_UUID }); + const uuid = await resolveInstanceUuid(configService, logger); + expect(uuid).toEqual(DEFAULT_CONFIG_UUID); + expect(writeFile).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Kibana instance UUID: CONFIG_UUID", + ] + `); + }); + }); + + describe('when file is not present and config property is set', () => { + it('writes the uuid to file and returns the config uuid', async () => { + mockReadFile({ error: fileNotFoundError }); + const uuid = await resolveInstanceUuid(configService, logger); + expect(uuid).toEqual(DEFAULT_CONFIG_UUID); + expect(writeFile).toHaveBeenCalledWith( + join('data-folder', 'uuid'), + DEFAULT_CONFIG_UUID, + expect.any(Object) + ); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Setting new Kibana instance UUID: CONFIG_UUID", + ] + `); + }); + }); + + describe('when file is present and config property is not set', () => { + it('does not write to file and returns the file uuid', async () => { + configService = getConfigService(undefined); + const uuid = await resolveInstanceUuid(configService, logger); + expect(uuid).toEqual(DEFAULT_FILE_UUID); + expect(writeFile).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Resuming persistent Kibana instance UUID: FILE_UUID", + ] + `); + }); + }); + + describe('when file is not present and config property is not set', () => { + it('generates a new uuid and write it to file', async () => { + configService = getConfigService(undefined); + mockReadFile({ error: fileNotFoundError }); + const uuid = await resolveInstanceUuid(configService, logger); + expect(uuid).toEqual('NEW_UUID'); + expect(writeFile).toHaveBeenCalledWith( + join('data-folder', 'uuid'), + 'NEW_UUID', + expect.any(Object) + ); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Setting new Kibana instance UUID: NEW_UUID", + ] + `); + }); + }); + + describe('when file access error occurs', () => { + it('throws an explicit error for file read errors', async () => { + mockReadFile({ error: permissionError }); + await expect( + resolveInstanceUuid(configService, logger) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to read Kibana UUID file, please check the uuid.server configuration value in kibana.yml and ensure Kibana has sufficient permissions to read / write to this file. Error was: EACCES"` + ); + }); + it('throws an explicit error for file write errors', async () => { + mockWriteFile(isDirectoryError); + await expect( + resolveInstanceUuid(configService, logger) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to write Kibana UUID file, please check the uuid.server configuration value in kibana.yml and ensure Kibana has sufficient permissions to read / write to this file. Error was: EISDIR"` + ); + }); + }); +}); diff --git a/src/core/server/uuid/resolve_uuid.ts b/src/core/server/uuid/resolve_uuid.ts new file mode 100644 index 0000000000000..17412bfa0544c --- /dev/null +++ b/src/core/server/uuid/resolve_uuid.ts @@ -0,0 +1,109 @@ +/* + * 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 uuid from 'uuid'; +import { promises } from 'fs'; +import { join } from 'path'; +import { take } from 'rxjs/operators'; +import { IConfigService } from '../config'; +import { PathConfigType, config as pathConfigDef } from '../path'; +import { HttpConfigType, config as httpConfigDef } from '../http'; +import { Logger } from '../logging'; + +const { readFile, writeFile } = promises; + +const FILE_ENCODING = 'utf8'; +const FILE_NAME = 'uuid'; + +export async function resolveInstanceUuid( + configService: IConfigService, + logger: Logger +): Promise { + const [pathConfig, serverConfig] = await Promise.all([ + configService + .atPath(pathConfigDef.path) + .pipe(take(1)) + .toPromise(), + configService + .atPath(httpConfigDef.path) + .pipe(take(1)) + .toPromise(), + ]); + + const uuidFilePath = join(pathConfig.data, FILE_NAME); + + const uuidFromFile = await readUuidFromFile(uuidFilePath); + const uuidFromConfig = serverConfig.uuid; + + if (uuidFromConfig) { + if (uuidFromConfig === uuidFromFile) { + // uuid matches, nothing to do + logger.debug(`Kibana instance UUID: ${uuidFromConfig}`); + return uuidFromConfig; + } else { + // uuid in file don't match, or file was not present, we need to write it. + if (uuidFromFile === undefined) { + logger.debug(`Setting new Kibana instance UUID: ${uuidFromConfig}`); + } else { + logger.debug(`Updating Kibana instance UUID to: ${uuidFromConfig} (was: ${uuidFromFile})`); + } + await writeUuidToFile(uuidFilePath, uuidFromConfig); + return uuidFromConfig; + } + } + if (uuidFromFile === undefined) { + const newUuid = uuid.v4(); + // no uuid either in config or file, we need to generate and write it. + logger.debug(`Setting new Kibana instance UUID: ${newUuid}`); + await writeUuidToFile(uuidFilePath, newUuid); + return newUuid; + } + + logger.debug(`Resuming persistent Kibana instance UUID: ${uuidFromFile}`); + return uuidFromFile; +} + +async function readUuidFromFile(filepath: string): Promise { + try { + const content = await readFile(filepath); + return content.toString(FILE_ENCODING); + } catch (e) { + if (e.code === 'ENOENT') { + // non-existent uuid file is ok, we will create it. + return undefined; + } + throw new Error( + 'Unable to read Kibana UUID file, please check the uuid.server configuration ' + + 'value in kibana.yml and ensure Kibana has sufficient permissions to read / write to this file. ' + + `Error was: ${e.code}` + ); + } +} + +async function writeUuidToFile(filepath: string, uuidValue: string) { + try { + return await writeFile(filepath, uuidValue, { encoding: FILE_ENCODING }); + } catch (e) { + throw new Error( + 'Unable to write Kibana UUID file, please check the uuid.server configuration ' + + 'value in kibana.yml and ensure Kibana has sufficient permissions to read / write to this file. ' + + `Error was: ${e.code}` + ); + } +} diff --git a/src/core/server/uuid/uuid_service.mock.ts b/src/core/server/uuid/uuid_service.mock.ts new file mode 100644 index 0000000000000..bf40eaee20636 --- /dev/null +++ b/src/core/server/uuid/uuid_service.mock.ts @@ -0,0 +1,41 @@ +/* + * 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 { UuidService, UuidServiceSetup } from './uuid_service'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + getInstanceUuid: jest.fn().mockImplementation(() => 'uuid'), + }; + return setupContract; +}; + +type UuidServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + }; + mocked.setup.mockResolvedValue(createSetupContractMock()); + return mocked; +}; + +export const uuidServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/uuid/uuid_service.test.ts b/src/core/server/uuid/uuid_service.test.ts new file mode 100644 index 0000000000000..315df7af8aa19 --- /dev/null +++ b/src/core/server/uuid/uuid_service.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { UuidService } from './uuid_service'; +import { resolveInstanceUuid } from './resolve_uuid'; +import { CoreContext } from '../core_context'; + +import { loggingServiceMock } from '../logging/logging_service.mock'; +import { mockCoreContext } from '../core_context.mock'; + +jest.mock('./resolve_uuid', () => ({ + resolveInstanceUuid: jest.fn().mockResolvedValue('SOME_UUID'), +})); + +describe('UuidService', () => { + let logger: ReturnType; + let coreContext: CoreContext; + let service: UuidService; + + beforeEach(() => { + jest.clearAllMocks(); + logger = loggingServiceMock.create(); + coreContext = mockCoreContext.create({ logger }); + service = new UuidService(coreContext); + }); + + describe('#setup()', () => { + it('calls manageInstanceUuid with core configuration service', async () => { + await service.setup(); + expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); + expect(resolveInstanceUuid).toHaveBeenCalledWith( + coreContext.configService, + logger.get('uuid') + ); + }); + + it('returns the uuid resolved from manageInstanceUuid', async () => { + const setup = await service.setup(); + expect(setup.getInstanceUuid()).toEqual('SOME_UUID'); + }); + }); +}); diff --git a/src/core/server/uuid/uuid_service.ts b/src/core/server/uuid/uuid_service.ts new file mode 100644 index 0000000000000..10104fa704936 --- /dev/null +++ b/src/core/server/uuid/uuid_service.ts @@ -0,0 +1,55 @@ +/* + * 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 { resolveInstanceUuid } from './resolve_uuid'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { IConfigService } from '../config'; + +/** + * APIs to access the application's instance uuid. + * + * @public + */ +export interface UuidServiceSetup { + /** + * Retrieve the Kibana instance uuid. + */ + getInstanceUuid(): string; +} + +/** @internal */ +export class UuidService { + private readonly log: Logger; + private readonly configService: IConfigService; + private uuid: string = ''; + + constructor(core: CoreContext) { + this.log = core.logger.get('uuid'); + this.configService = core.configService; + } + + public async setup() { + this.uuid = await resolveInstanceUuid(this.configService, this.log); + + return { + getInstanceUuid: () => this.uuid, + }; + } +} diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 93167cb0e6bf9..8dc470e20c619 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -22,7 +22,6 @@ import { resolve } from 'path'; import { promisify } from 'util'; import { migrations } from './migrations'; -import manageUuid from './server/lib/manage_uuid'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import { homeApi } from './server/routes/api/home'; @@ -326,8 +325,6 @@ export default function(kibana) { init: async function(server) { const { usageCollection } = server.newPlatform.setup.plugins; - // uuid - await manageUuid(server); // routes scriptsApi(server); importApi(server); diff --git a/src/legacy/core_plugins/kibana/server/lib/__tests__/manage_uuid.js b/src/legacy/core_plugins/kibana/server/lib/__tests__/manage_uuid.js deleted file mode 100644 index 852e75876ad15..0000000000000 --- a/src/legacy/core_plugins/kibana/server/lib/__tests__/manage_uuid.js +++ /dev/null @@ -1,102 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { createTestServers } from '../../../../../../test_utils/kbn_server'; -import manageUuid from '../manage_uuid'; - -describe('legacy/core_plugins/kibana/server/lib', function() { - describe('manage_uuid', function() { - const testUuid = 'c4add484-0cba-4e05-86fe-4baa112d9e53'; - let kbn; - let kbnServer; - let esServer; - let config; - let servers; - - before(async function() { - servers = createTestServers({ - adjustTimeout: t => { - this.timeout(t); - }, - }); - esServer = await servers.startES(); - - kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; - }); - - // Clear uuid stuff from previous test runs - beforeEach(function() { - kbnServer.server.log = sinon.stub(); - config = kbnServer.server.config(); - }); - - after(() => { - esServer.stop(); - kbn.stop(); - }); - - it('ensure config uuid is validated as a guid', async function() { - config.set('server.uuid', testUuid); - expect(config.get('server.uuid')).to.be(testUuid); - - expect(() => { - config.set('server.uuid', 'foouid'); - }).to.throwException(e => { - expect(e.name).to.be('ValidationError'); - }); - }); - - it('finds the previously set uuid with config match', async function() { - const msg = `Kibana instance UUID: ${testUuid}`; - config.set('server.uuid', testUuid); - - await manageUuid(kbnServer.server); - await manageUuid(kbnServer.server); - - expect(kbnServer.server.log.lastCall.args[1]).to.be.eql(msg); - }); - - it('updates the previously set uuid with config value', async function() { - config.set('server.uuid', testUuid); - - await manageUuid(kbnServer.server); - - const newUuid = '5b2de169-2785-441b-ae8c-186a1936b17d'; - const msg = `Updating Kibana instance UUID to: ${newUuid} (was: ${testUuid})`; - - config.set('server.uuid', newUuid); - await manageUuid(kbnServer.server); - - expect(kbnServer.server.log.lastCall.args[1]).to.be(msg); - }); - - it('resumes the uuid stored in data and sets it to the config', async function() { - const partialMsg = 'Resuming persistent Kibana instance UUID'; - config.set('server.uuid'); // set to undefined - - await manageUuid(kbnServer.server); - - expect(config.get('server.uuid')).to.be.ok(); // not undefined any more - expect(kbnServer.server.log.lastCall.args[1]).to.match(new RegExp(`^${partialMsg}`)); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/server/lib/manage_uuid.js b/src/legacy/core_plugins/kibana/server/lib/manage_uuid.js deleted file mode 100644 index adae8517287d6..0000000000000 --- a/src/legacy/core_plugins/kibana/server/lib/manage_uuid.js +++ /dev/null @@ -1,97 +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 uuid from 'uuid'; -import Bluebird from 'bluebird'; -import { join as pathJoin } from 'path'; -import { readFile as readFileCallback, writeFile as writeFileCallback } from 'fs'; - -const FILE_ENCODING = 'utf8'; - -export default async function manageUuid(server) { - const config = server.config(); - const fileName = 'uuid'; - const uuidFile = pathJoin(config.get('path.data'), fileName); - - async function detectUuid() { - const readFile = Bluebird.promisify(readFileCallback); - try { - const result = await readFile(uuidFile); - return result.toString(FILE_ENCODING); - } catch (err) { - if (err.code === 'ENOENT') { - // non-existent uuid file is ok - return false; - } - server.log(['error', 'read-uuid'], err); - // Note: this will most likely be logged as an Unhandled Rejection - throw err; - } - } - - async function writeUuid(uuid) { - const writeFile = Bluebird.promisify(writeFileCallback); - try { - return await writeFile(uuidFile, uuid, { encoding: FILE_ENCODING }); - } catch (err) { - server.log(['error', 'write-uuid'], err); - // Note: this will most likely be logged as an Unhandled Rejection - throw err; - } - } - - // detect if uuid exists already from before a restart - const logToServer = msg => server.log(['server', 'uuid', fileName], msg); - const dataFileUuid = await detectUuid(); - let serverConfigUuid = config.get('server.uuid'); // check if already set in config - - if (dataFileUuid) { - // data uuid found - if (serverConfigUuid === dataFileUuid) { - // config uuid exists, data uuid exists and matches - logToServer(`Kibana instance UUID: ${dataFileUuid}`); - return; - } - - if (!serverConfigUuid) { - // config uuid missing, data uuid exists - serverConfigUuid = dataFileUuid; - logToServer(`Resuming persistent Kibana instance UUID: ${serverConfigUuid}`); - config.set('server.uuid', serverConfigUuid); - return; - } - - if (serverConfigUuid !== dataFileUuid) { - // config uuid exists, data uuid exists but mismatches - logToServer(`Updating Kibana instance UUID to: ${serverConfigUuid} (was: ${dataFileUuid})`); - return writeUuid(serverConfigUuid); - } - } - - // data uuid missing - - if (!serverConfigUuid) { - // config uuid missing - serverConfigUuid = uuid.v4(); - config.set('server.uuid', serverConfigUuid); - } - - logToServer(`Setting new Kibana instance UUID: ${serverConfigUuid}`); - return writeUuid(serverConfigUuid); -} diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index bfbb51e1f5f12..b31939adee6bc 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -69,9 +69,6 @@ export default () => }), server: Joi.object({ - uuid: Joi.string() - .guid() - .default(), name: Joi.string().default(os.hostname()), defaultRoute: Joi.string().regex(/^\//, `start with a slash`), customResponseHeaders: Joi.object() @@ -111,6 +108,7 @@ export default () => socketTimeout: HANDLED_IN_NEW_PLATFORM, ssl: HANDLED_IN_NEW_PLATFORM, compression: HANDLED_IN_NEW_PLATFORM, + uuid: HANDLED_IN_NEW_PLATFORM, }).default(), uiSettings: HANDLED_IN_NEW_PLATFORM, diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index ea6d12dd8cdeb..013019fb24f31 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -45,7 +45,8 @@ import { IndexPatternsServiceFactory } from './index_patterns'; import { Capabilities } from '../../core/server'; import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; -export type KibanaConfig = LegacyConfig; +// lot of legacy code was assuming this type only had these two methods +export type KibanaConfig = Pick; export interface UiApp { getId(): string;