Migrate instance uuid logic to new platform (#52060) (#53260)

* 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
This commit is contained in:
Pierre Gayvallet 2019-12-17 11:16:38 +01:00 committed by GitHub
parent e69183d9f6
commit 805179726f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 742 additions and 233 deletions

View file

@ -22,4 +22,5 @@ export interface CoreSetup
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) |
| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) |
| [uuid](./kibana-plugin-server.coresetup.uuid.md) | <code>UuidServiceSetup</code> | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md) &gt; [uuid](./kibana-plugin-server.coresetup.uuid.md)
## CoreSetup.uuid property
[UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md)
<b>Signature:</b>
```typescript
uuid: UuidServiceSetup;
```

View file

@ -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

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) &gt; [getInstanceUuid](./kibana-plugin-server.uuidservicesetup.getinstanceuuid.md)
## UuidServiceSetup.getInstanceUuid() method
Retrieve the Kibana instance uuid.
<b>Signature:</b>
```typescript
getInstanceUuid(): string;
```
<b>Returns:</b>
`string`

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md)
## UuidServiceSetup interface
APIs to access the application's instance uuid.
<b>Signature:</b>
```typescript
export interface UuidServiceSetup
```
## Methods
| Method | Description |
| --- | --- |
| [getInstanceUuid()](./kibana-plugin-server.uuidservicesetup.getinstanceuuid.md) | Retrieve the Kibana instance uuid. |

View file

@ -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;

View file

@ -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 => {

View file

@ -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,
};

View file

@ -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;
}
/**

View file

@ -20,6 +20,7 @@ Object {
"keyPassphrase": "some-phrase",
"someNewValue": "new",
},
"uuid": undefined,
}
`;
@ -43,6 +44,7 @@ Object {
"enabled": false,
"key": "key",
},
"uuid": undefined,
}
`;

View file

@ -73,6 +73,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
keepaliveTimeout: configValue.keepaliveTimeout,
socketTimeout: configValue.socketTimeout,
compression: configValue.compression,
uuid: configValue.uuid,
};
}

View file

@ -25,6 +25,8 @@
export interface LegacyConfig {
get<T>(key?: string): T;
has(key: string): boolean;
set(key: string, value: any): void;
set(config: Record<string, any>): void;
}
/**

View file

@ -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<LegacyServiceDiscoverPlugins> = {
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,
};

View file

@ -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<string, any>) => ({
pluginSpecs: [],
pluginExtendedConfig: {
has: jest.fn(),
get: jest.fn(() => settings),
set: jest.fn(),
},
disabledPluginSpecs: [],
uiExports: [],
}));
jest.doMock('./plugins/find_legacy_plugin_specs.ts', () => ({
findLegacyPluginSpecs: findLegacyPluginSpecsMock,
}));

View file

@ -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> = KbnServer as any;
const findLegacyPluginSpecsMock: jest.Mock<typeof findLegacyPluginSpecs> = findLegacyPluginSpecs as any;
let coreId: symbol;
let env: Env;
@ -58,23 +56,17 @@ let startDeps: LegacyServiceStartDeps;
const logger = loggingServiceMock.create();
let configService: ReturnType<typeof configServiceMock.create>;
let uuidSetup: ReturnType<typeof uuidServiceMock.createSetupContract>;
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<string, any>) => ({
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');
});

View file

@ -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,

View file

@ -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<T>(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;
}

View file

@ -173,6 +173,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
uiSettings: {
register: deps.uiSettings.register,
},
uuid: {
getInstanceUuid: deps.uuid.getInstanceUuid,
},
};
}

View file

@ -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,

View file

@ -564,6 +564,8 @@ export interface CoreSetup {
savedObjects: SavedObjectsServiceSetup;
// (undocumented)
uiSettings: UiSettingsServiceSetup;
// (undocumented)
uuid: UuidServiceSetup;
}
// @public
@ -1831,6 +1833,11 @@ export interface UserProvidedValues<T = any> {
userValue?: T;
}
// @public
export interface UuidServiceSetup {
getInstanceUuid(): string;
}
// @public
export const validBodyOutput: readonly ["data", "stream"];

View file

@ -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),
}));

View file

@ -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);

View file

@ -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';

View file

@ -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<typeof configServiceMock.create>;
let logger: jest.Mocked<Logger>;
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"`
);
});
});
});

View file

@ -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<string> {
const [pathConfig, serverConfig] = await Promise.all([
configService
.atPath<PathConfigType>(pathConfigDef.path)
.pipe(take(1))
.toPromise(),
configService
.atPath<HttpConfigType>(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<string | undefined> {
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}`
);
}
}

View file

@ -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<UuidServiceSetup> = {
getInstanceUuid: jest.fn().mockImplementation(() => 'uuid'),
};
return setupContract;
};
type UuidServiceContract = PublicMethodsOf<UuidService>;
const createMock = () => {
const mocked: jest.Mocked<UuidServiceContract> = {
setup: jest.fn(),
};
mocked.setup.mockResolvedValue(createSetupContractMock());
return mocked;
};
export const uuidServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
};

View file

@ -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<typeof loggingServiceMock.create>;
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');
});
});
});

View file

@ -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,
};
}
}

View file

@ -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);

View file

@ -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}`));
});
});
});

View file

@ -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);
}

View file

@ -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,

View file

@ -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<LegacyConfig, 'get' | 'has'>;
export interface UiApp {
getId(): string;