From 1bb4d77479dbf5b21515378f7ab9c5d1349d4052 Mon Sep 17 00:00:00 2001 From: Kerg09 <46001886+Kerg09@users.noreply.github.com> Date: Fri, 31 Jan 2020 10:45:37 -0600 Subject: [PATCH 1/8] Adds Query.AI to known-plugins.asciidoc (#50516) Co-authored-by: Elastic Machine --- docs/plugins/known-plugins.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index cc27eca4c267e..cb70a1d1c387a 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -20,6 +20,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API * https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. * https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules +* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights [float] === Timelion Extensions From f1068cdbff1c246c6a0154a803435a5585ef7cea Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 31 Jan 2020 17:49:33 +0100 Subject: [PATCH 2/8] Perform successful Elasticsearch version check before migrations (#51311) * Convert parts of Elasticsearch version check to ts * Move ES version check to NP * Improve types * Wait till for compatible ES nodes before SO migrations * Don't wait for ES compatibility if skipMigrations=true * Legacy Elasticsearch plugin integration test * Make ES compatibility check and migrations logging more visible * Test for isCompatible=false when ES version check throws * Start pollEsNodesVersion immediately * Refactor pollEsNodesVersion --- .../elasticsearch/elasticsearch_config.ts | 14 +- .../elasticsearch_service.mock.ts | 7 + .../elasticsearch_service.test.ts | 11 +- .../elasticsearch/elasticsearch_service.ts | 46 ++- src/core/server/elasticsearch/types.ts | 2 + .../version_check/ensure_es_version.test.ts | 261 ++++++++++++++++++ .../version_check/ensure_es_version.ts | 164 +++++++++++ .../es_kibana_version_compatability.test.ts} | 18 +- .../es_kibana_version_compatability.ts} | 15 +- .../integration_tests/core_services.test.ts | 8 +- .../integration_tests/legacy_service.test.ts | 2 +- .../migrations/kibana/kibana_migrator.test.ts | 6 - .../migrations/kibana/kibana_migrator.ts | 15 +- .../saved_objects_service.test.ts | 41 ++- .../saved_objects/saved_objects_service.ts | 25 +- .../core_plugins/elasticsearch/index.d.ts | 1 + .../core_plugins/elasticsearch/index.js | 14 +- .../integration_tests/elasticsearch.test.ts | 89 ++++++ .../version_health_check.js} | 24 +- .../lib/version_health_check.test.js | 71 +++++ .../server/lib/__tests__/ensure_es_version.js | 223 --------------- .../server/lib/__tests__/health_check.js | 151 ---------- .../server/lib/ensure_es_version.js | 126 --------- .../elasticsearch/server/lib/health_check.js | 75 ----- .../default_route_provider.test.ts | 2 +- .../default_route_provider_config.test.ts | 1 + .../max_payload_size.test.js | 2 +- .../on_post_auth_interceptor.test.ts | 5 +- .../on_request_interceptor.test.ts | 5 +- 29 files changed, 769 insertions(+), 655 deletions(-) create mode 100644 src/core/server/elasticsearch/version_check/ensure_es_version.test.ts create mode 100644 src/core/server/elasticsearch/version_check/ensure_es_version.ts rename src/{legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js => core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts} (72%) rename src/{legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js => core/server/elasticsearch/version_check/es_kibana_version_compatability.ts} (76%) create mode 100644 src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts rename src/legacy/core_plugins/elasticsearch/{server/lib/kibana_version.js => lib/version_health_check.js} (57%) create mode 100644 src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/health_check.js diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 5f06c51a53d53..50866e5550d8e 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -103,7 +103,19 @@ const configSchema = schema.object({ ), apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), - ignoreVersionMismatch: schema.boolean({ defaultValue: false }), + ignoreVersionMismatch: schema.conditional( + schema.contextRef('dev'), + false, + schema.boolean({ + validate: rawValue => { + if (rawValue === true) { + return '"ignoreVersionMismatch" can only be set to true in development mode'; + } + }, + defaultValue: false, + }), + schema.boolean({ defaultValue: false }) + ), }); const deprecations: ConfigDeprecationProvider = () => [ diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index a4e51ca55b3e7..b8ad375496544 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -23,6 +23,7 @@ import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup } from './types'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; const createScopedClusterClientMock = (): jest.Mocked => ({ callAsInternalUser: jest.fn(), @@ -71,6 +72,12 @@ type MockedInternalElasticSearchServiceSetup = jest.Mocked< const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { ...createSetupContractMock(), + esNodesCompatibility$: new BehaviorSubject({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), }, diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 5a7d223fec7ad..022a03e01d37d 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -31,6 +31,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; +import { duration } from 'moment'; let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); @@ -41,7 +42,7 @@ configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], healthCheck: { - delay: 2000, + delay: duration(2000), }, ssl: { verificationMode: 'none', @@ -125,7 +126,7 @@ describe('#setup', () => { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://8.8.8.8", ], @@ -150,7 +151,7 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://1.2.3.4", ], @@ -174,7 +175,7 @@ Object { new BehaviorSubject({ hosts: ['http://1.2.3.4', 'http://9.8.7.6'], healthCheck: { - delay: 2000, + delay: duration(2000), }, ssl: { verificationMode: 'none', @@ -196,7 +197,7 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://8.8.8.8", ], diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index de111e1cb8b9b..9eaf125cc006f 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -30,6 +30,7 @@ import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_co import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { InternalElasticsearchServiceSetup } from './types'; import { CallAPIOptions } from './api_types'; +import { pollEsNodesVersion } from './version_check/ensure_es_version'; /** @internal */ interface CoreClusterClients { @@ -46,9 +47,17 @@ interface SetupDeps { export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private subscription?: Subscription; + private subscriptions: { + client?: Subscription; + esNodesCompatibility?: Subscription; + } = { + client: undefined, + esNodesCompatibility: undefined, + }; + private kibanaVersion: string; constructor(private readonly coreContext: CoreContext) { + this.kibanaVersion = coreContext.env.packageInfo.version; this.log = coreContext.logger.get('elasticsearch-service'); this.config$ = coreContext.configService .atPath('elasticsearch') @@ -60,7 +69,7 @@ export class ElasticsearchService implements CoreService { - if (this.subscription !== undefined) { + if (this.subscriptions.client !== undefined) { this.log.error('Clients cannot be changed after they are created'); return false; } @@ -91,7 +100,7 @@ export class ElasticsearchService implements CoreService; - this.subscription = clients$.connect(); + this.subscriptions.client = clients$.connect(); const config = await this.config$.pipe(first()).toPromise(); @@ -149,11 +158,31 @@ export class ElasticsearchService implements CoreService).connect(); + + // TODO: Move to Status Service https://github.com/elastic/kibana/issues/41983 + esNodesCompatibility$.subscribe(({ isCompatible, message }) => { + if (!isCompatible && message) { + this.log.error(message); + } + }); + return { legacy: { config$: clients$.pipe(map(clients => clients.config)) }, adminClient, dataClient, + esNodesCompatibility$, createClient: (type: string, clientConfig: Partial = {}) => { const finalConfig = merge({}, config, clientConfig); @@ -166,11 +195,12 @@ export class ElasticsearchService implements CoreService; }; + esNodesCompatibility$: Observable; } diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts new file mode 100644 index 0000000000000..4989c4a31295c --- /dev/null +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -0,0 +1,261 @@ +/* + * 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 { mapNodesVersionCompatibility, pollEsNodesVersion, NodesInfo } from './ensure_es_version'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { take, delay } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { of } from 'rxjs'; + +const mockLoggerFactory = loggingServiceMock.create(); +const mockLogger = mockLoggerFactory.get('mock logger'); + +const KIBANA_VERSION = '5.1.0'; + +function createNodes(...versions: string[]): NodesInfo { + const nodes = {} as any; + versions + .map(version => { + return { + version, + http: { + publish_address: 'http_address', + }, + ip: 'ip', + }; + }) + .forEach((node, i) => { + nodes[`node-${i}`] = node; + }); + + return { nodes }; +} + +describe('mapNodesVersionCompatibility', () => { + function createNodesInfoWithoutHTTP(version: string): NodesInfo { + return { nodes: { 'node-without-http': { version, ip: 'ip' } } } as any; + } + + it('returns isCompatible=true with a single node that matches', async () => { + const nodesInfo = createNodes('5.1.0'); + const result = await mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + }); + + it('returns isCompatible=true with multiple nodes that satisfy', async () => { + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.1.1-Beta1'); + const result = await mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + }); + + it('returns isCompatible=false for a single node that is out of date', () => { + // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.0.0'); + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.message).toMatchInlineSnapshot( + `"This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=false for an incompatible node without http publish address', async () => { + const nodesInfo = createNodesInfoWithoutHTTP('6.1.1'); + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.message).toMatchInlineSnapshot( + `"This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v6.1.1 @ undefined (ip)"` + ); + }); + + it('returns isCompatible=true for outdated nodes when ignoreVersionMismatch=true', async () => { + // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.0.0'); + const ignoreVersionMismatch = true; + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, ignoreVersionMismatch); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"Ignoring version incompatibility between Kibana v5.1.0 and the following Elasticsearch nodes: v5.0.0 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=true with a message if a node is only off by a patch version', () => { + const result = mapNodesVersionCompatibility(createNodes('5.1.1'), KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=true with a message if a node is only off by a patch version and without http publish address', async () => { + const result = mapNodesVersionCompatibility(createNodes('5.1.1'), KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` + ); + }); +}); + +describe('pollEsNodesVersion', () => { + const callWithInternalUser = jest.fn(); + const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + beforeEach(() => { + callWithInternalUser.mockClear(); + }); + + it('returns iscCompatible=false and keeps polling when a poll request throws', done => { + expect.assertions(3); + const expectedCompatibilityResults = [false, false, true]; + jest.clearAllMocks(); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); + callWithInternalUser.mockRejectedValueOnce(new Error('mock request error')); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(3)) + .subscribe({ + next: result => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns compatibility results', done => { + expect.assertions(1); + const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); + callWithInternalUser.mockResolvedValueOnce(nodes); + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: result => { + expect(result).toEqual(mapNodesVersionCompatibility(nodes, KIBANA_VERSION, false)); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the node versions changed since the previous poll', done => { + expect.assertions(4); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.0.0', '5.1.0', '5.2.0')); // ignore, same versions, different ordering + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.2.0', '5.0.0')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.1.2', '5.1.3')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.1.2', '5.1.3')); // ignore + callWithInternalUser.mockResolvedValueOnce(createNodes('5.0.0', '5.1.0', '5.2.0')); // emit, different from previous version + + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: result => expect(result).toBeDefined(), + complete: done, + error: done, + }); + }); + + it('starts polling immediately and then every esVersionCheckInterval', () => { + expect.assertions(1); + callWithInternalUser.mockReturnValueOnce([createNodes('5.1.0', '5.2.0', '5.0.0')]); + callWithInternalUser.mockReturnValueOnce([createNodes('5.1.1', '5.2.0', '5.0.0')]); + + getTestScheduler().run(({ expectObservable }) => { + const expected = 'a 99ms (b|)'; + + const esNodesCompatibility$ = pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 100, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }).pipe(take(2)); + + expectObservable(esNodesCompatibility$).toBe(expected, { + a: mapNodesVersionCompatibility( + createNodes('5.1.0', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + b: mapNodesVersionCompatibility( + createNodes('5.1.1', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + }); + }); + }); + + it('waits for es version check requests to complete before scheduling the next one', () => { + expect.assertions(2); + + getTestScheduler().run(({ expectObservable }) => { + const expected = '100ms a 99ms (b|)'; + + callWithInternalUser.mockReturnValueOnce( + of(createNodes('5.1.0', '5.2.0', '5.0.0')).pipe(delay(100)) + ); + callWithInternalUser.mockReturnValueOnce( + of(createNodes('5.1.1', '5.2.0', '5.0.0')).pipe(delay(100)) + ); + + const esNodesCompatibility$ = pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 10, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }).pipe(take(2)); + + expectObservable(esNodesCompatibility$).toBe(expected, { + a: mapNodesVersionCompatibility( + createNodes('5.1.0', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + b: mapNodesVersionCompatibility( + createNodes('5.1.1', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + }); + }); + + expect(callWithInternalUser).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts new file mode 100644 index 0000000000000..3e760ec0efabd --- /dev/null +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -0,0 +1,164 @@ +/* + * 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. + */ + +/** + * ES and Kibana versions are locked, so Kibana should require that ES has the same version as + * that defined in Kibana's package.json. + */ + +import { timer, of, from, Observable } from 'rxjs'; +import { map, distinctUntilChanged, catchError, exhaustMap } from 'rxjs/operators'; +import { + esVersionCompatibleWithKibana, + esVersionEqualsKibana, +} from './es_kibana_version_compatability'; +import { Logger } from '../../logging'; +import { APICaller } from '..'; + +export interface PollEsNodesVersionOptions { + callWithInternalUser: APICaller; + log: Logger; + kibanaVersion: string; + ignoreVersionMismatch: boolean; + esVersionCheckInterval: number; +} + +interface NodeInfo { + version: string; + ip: string; + http: { + publish_address: string; + }; + name: string; +} + +export interface NodesInfo { + nodes: { + [key: string]: NodeInfo; + }; +} + +export interface NodesVersionCompatibility { + isCompatible: boolean; + message?: string; + incompatibleNodes: NodeInfo[]; + warningNodes: NodeInfo[]; + kibanaVersion: string; +} + +function getHumanizedNodeName(node: NodeInfo) { + const publishAddress = node?.http?.publish_address + ' ' || ''; + return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; +} + +export function mapNodesVersionCompatibility( + nodesInfo: NodesInfo, + kibanaVersion: string, + ignoreVersionMismatch: boolean +): NodesVersionCompatibility { + if (Object.keys(nodesInfo.nodes).length === 0) { + return { + isCompatible: false, + message: 'Unable to retrieve version information from Elasticsearch nodes.', + incompatibleNodes: [], + warningNodes: [], + kibanaVersion, + }; + } + const nodes = Object.keys(nodesInfo.nodes) + .sort() // Sorting ensures a stable node ordering for comparison + .map(key => nodesInfo.nodes[key]) + .map(node => Object.assign({}, node, { name: getHumanizedNodeName(node) })); + + // Aggregate incompatible ES nodes. + const incompatibleNodes = nodes.filter( + node => !esVersionCompatibleWithKibana(node.version, kibanaVersion) + ); + + // Aggregate ES nodes which should prompt a Kibana upgrade. It's acceptable + // if ES and Kibana versions are not the same as long as they are not + // incompatible, but we should warn about it. + // Ignore version qualifiers https://github.com/elastic/elasticsearch/issues/36859 + const warningNodes = nodes.filter(node => !esVersionEqualsKibana(node.version, kibanaVersion)); + + // Note: If incompatible and warning nodes are present `message` only contains + // an incompatibility notice. + let message; + if (incompatibleNodes.length > 0) { + const incompatibleNodeNames = incompatibleNodes.map(node => node.name).join(', '); + if (ignoreVersionMismatch) { + message = `Ignoring version incompatibility between Kibana v${kibanaVersion} and the following Elasticsearch nodes: ${incompatibleNodeNames}`; + } else { + message = `This version of Kibana (v${kibanaVersion}) is incompatible with the following Elasticsearch nodes in your cluster: ${incompatibleNodeNames}`; + } + } else if (warningNodes.length > 0) { + const warningNodeNames = warningNodes.map(node => node.name).join(', '); + message = + `You're running Kibana ${kibanaVersion} with some different versions of ` + + 'Elasticsearch. Update Kibana or Elasticsearch to the same ' + + `version to prevent compatibility issues: ${warningNodeNames}`; + } + + return { + isCompatible: ignoreVersionMismatch || incompatibleNodes.length === 0, + message, + incompatibleNodes, + warningNodes, + kibanaVersion, + }; +} + +// Returns true if two NodesVersionCompatibility entries match +function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { + const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; + return ( + curr.isCompatible === prev.isCompatible && + curr.incompatibleNodes.length === prev.incompatibleNodes.length && + curr.warningNodes.length === prev.warningNodes.length && + curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + ); +} + +export const pollEsNodesVersion = ({ + callWithInternalUser, + log, + kibanaVersion, + ignoreVersionMismatch, + esVersionCheckInterval: healthCheckInterval, +}: PollEsNodesVersionOptions): Observable => { + log.debug('Checking Elasticsearch version'); + return timer(0, healthCheckInterval).pipe( + exhaustMap(() => { + return from( + callWithInternalUser('nodes.info', { + filterPath: ['nodes.*.version', 'nodes.*.http.publish_address', 'nodes.*.ip'], + }) + ).pipe( + catchError(_err => { + return of({ nodes: {} }); + }) + ); + }), + map((nodesInfo: NodesInfo) => + mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + ), + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + ); +}; diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts similarity index 72% rename from src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js rename to src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts index 092c0ecf1071c..152f25c813881 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js +++ b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts @@ -17,41 +17,39 @@ * under the License. */ -import expect from '@kbn/expect'; - -import isEsCompatibleWithKibana from '../is_es_compatible_with_kibana'; +import { esVersionCompatibleWithKibana } from './es_kibana_version_compatability'; describe('plugins/elasticsearch', () => { describe('lib/is_es_compatible_with_kibana', () => { describe('returns false', () => { it('when ES major is greater than Kibana major', () => { - expect(isEsCompatibleWithKibana('1.0.0', '0.0.0')).to.be(false); + expect(esVersionCompatibleWithKibana('1.0.0', '0.0.0')).toBe(false); }); it('when ES major is less than Kibana major', () => { - expect(isEsCompatibleWithKibana('0.0.0', '1.0.0')).to.be(false); + expect(esVersionCompatibleWithKibana('0.0.0', '1.0.0')).toBe(false); }); it('when majors are equal, but ES minor is less than Kibana minor', () => { - expect(isEsCompatibleWithKibana('1.0.0', '1.1.0')).to.be(false); + expect(esVersionCompatibleWithKibana('1.0.0', '1.1.0')).toBe(false); }); }); describe('returns true', () => { it('when version numbers are the same', () => { - expect(isEsCompatibleWithKibana('1.1.1', '1.1.1')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.1', '1.1.1')).toBe(true); }); it('when majors are equal, and ES minor is greater than Kibana minor', () => { - expect(isEsCompatibleWithKibana('1.1.0', '1.0.0')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.0', '1.0.0')).toBe(true); }); it('when majors and minors are equal, and ES patch is greater than Kibana patch', () => { - expect(isEsCompatibleWithKibana('1.1.1', '1.1.0')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.1', '1.1.0')).toBe(true); }); it('when majors and minors are equal, but ES patch is less than Kibana patch', () => { - expect(isEsCompatibleWithKibana('1.1.0', '1.1.1')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.0', '1.1.1')).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts similarity index 76% rename from src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js rename to src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts index 4afbd488d2946..28b9c0a23e672 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js +++ b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts @@ -17,15 +17,14 @@ * under the License. */ +import semver, { coerce } from 'semver'; + /** - * Let's weed out the ES versions that won't work with a given Kibana version. + * Checks for the compatibilitiy between Elasticsearch and Kibana versions * 1. Major version differences will never work together. * 2. Older versions of ES won't work with newer versions of Kibana. */ - -import semver from 'semver'; - -export default function isEsCompatibleWithKibana(esVersion, kibanaVersion) { +export function esVersionCompatibleWithKibana(esVersion: string, kibanaVersion: string) { const esVersionNumbers = { major: semver.major(esVersion), minor: semver.minor(esVersion), @@ -50,3 +49,9 @@ export default function isEsCompatibleWithKibana(esVersion, kibanaVersion) { return true; } + +export function esVersionEqualsKibana(nodeVersion: string, kibanaVersion: string) { + const nodeSemVer = coerce(nodeVersion); + const kibanaSemver = coerce(kibanaVersion); + return nodeSemVer && kibanaSemver && nodeSemVer.version === kibanaSemver.version; +} diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 65b8ba551cf91..425d8cac1893e 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -43,7 +43,7 @@ describe('http service', () => { describe('auth', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { @@ -161,7 +161,7 @@ describe('http service', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { @@ -295,7 +295,7 @@ describe('http service', () => { describe('#basePath()', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => await root.shutdown()); @@ -324,7 +324,7 @@ describe('http service', () => { describe('elasticsearch', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index da2550f2ae799..e8bcf7a42d192 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -22,7 +22,7 @@ describe('legacy service', () => { describe('http server', () => { let root: ReturnType; beforeEach(() => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => await root.shutdown()); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index b89abc596ad18..c6a72eb53d6c4 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -59,12 +59,6 @@ describe('KibanaMigrator', () => { }); describe('runMigrations', () => { - it('resolves isMigrated if migrations were skipped', async () => { - const skipMigrations = true; - const result = await new KibanaMigrator(mockOptions()).runMigrations(skipMigrations); - expect(result).toEqual([{ status: 'skipped' }, { status: 'skipped' }]); - }); - it('only runs migrations once if called multiple times', async () => { const options = mockOptions(); const clusterStub = jest.fn(() => ({ status: 404 })); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index c35e8dd90b5b1..747b48a540109 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -107,24 +107,15 @@ export class KibanaMigrator { * The promise resolves with an array of migration statuses, one for each * elasticsearch index which was migrated. */ - public runMigrations(skipMigrations: boolean = false): Promise> { + public runMigrations(): Promise> { if (this.migrationResult === undefined) { - this.migrationResult = this.runMigrationsInternal(skipMigrations); + this.migrationResult = this.runMigrationsInternal(); } return this.migrationResult; } - private runMigrationsInternal(skipMigrations: boolean) { - if (skipMigrations) { - this.log.warn( - 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' - ); - return Promise.resolve( - Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })) - ); - } - + private runMigrationsInternal() { const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ config: this.config, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 6668d57045a95..19798aa68928d 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -31,11 +31,14 @@ import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { legacyServiceMock } from '../legacy/legacy_service.mock'; import { SavedObjectsClientFactoryProvider } from './service/lib'; +import { BehaviorSubject } from 'rxjs'; +import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; describe('SavedObjectsService', () => { const createSetupDeps = () => { + const elasticsearchMock = elasticsearchServiceMock.createInternalSetup(); return { - elasticsearch: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, legacyPlugins: legacyServiceMock.createDiscoverPlugins(), }; }; @@ -137,7 +140,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); await soService.start({}); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(true); + expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); it('skips KibanaMigrator migrations when migrations.skip=true', async () => { @@ -146,7 +149,38 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); await soService.start({}); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(true); + expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); + }); + + it('waits for all es nodes to be compatible before running migrations', async done => { + expect.assertions(2); + const configService = configServiceMock.create({ atPath: { skip: false } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const setupDeps = createSetupDeps(); + // Create an new subject so that we can control when isCompatible=true + // is emitted. + setupDeps.elasticsearch.esNodesCompatibility$ = new BehaviorSubject({ + isCompatible: false, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + await soService.setup(setupDeps); + soService.start({}); + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); + ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< + NodesVersionCompatibility + >).next({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + setImmediate(() => { + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); + done(); + }); }); it('resolves with KibanaMigrator after waiting for migrations to complete', async () => { @@ -158,7 +192,6 @@ describe('SavedObjectsService', () => { const startContract = await soService.start({}); expect(startContract.migrator).toBe(migratorInstanceMock); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(false); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b08033a19242b..0c985c71c7e2f 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -18,7 +18,7 @@ */ import { CoreService } from 'src/core/types'; -import { first } from 'rxjs/operators'; +import { first, filter, take } from 'rxjs/operators'; import { SavedObjectsClient, SavedObjectsSchema, @@ -283,9 +283,22 @@ export class SavedObjectsService const cliArgs = this.coreContext.env.cliArgs; const skipMigrations = cliArgs.optimize || savedObjectsConfig.skip; - this.logger.debug('Starting saved objects migration'); - await migrator.runMigrations(skipMigrations); - this.logger.debug('Saved objects migration completed'); + if (skipMigrations) { + this.logger.warn( + 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' + ); + } else { + this.logger.info( + 'Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...' + ); + await this.setupDeps!.elasticsearch.esNodesCompatibility$.pipe( + filter(nodes => nodes.isCompatible), + take(1) + ).toPromise(); + + this.logger.info('Starting saved objects migrations'); + await migrator.runMigrations(); + } const createRepository = (callCluster: APICaller, extraTypes: string[] = []) => { return SavedObjectsRepository.createRepository( @@ -343,14 +356,14 @@ export class SavedObjectsService savedObjectMappings: this.mappings, savedObjectMigrations: this.migrations, savedObjectValidations: this.validations, - logger: this.coreContext.logger.get('migrations'), + logger: this.logger, kibanaVersion: this.coreContext.env.packageInfo.version, config: this.setupDeps!.legacyPlugins.pluginExtendedConfig, savedObjectsConfig, kibanaConfig, callCluster: migrationsRetryCallCluster( adminClient.callAsInternalUser, - this.coreContext.logger.get('migrations'), + this.logger, migrationsRetryDelay ), }); diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index 4cbb1c82cc1e4..df713160137a6 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -523,6 +523,7 @@ export interface CallCluster { } export interface ElasticsearchPlugin { + status: { on: (status: string, cb: () => void) => void }; getCluster(name: string): Cluster; createCluster(name: string, config: ClusterConfig): Cluster; waitUntilReady(): Promise; diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index 5872a33d8aa08..35dd6562aed98 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -17,10 +17,10 @@ * under the License. */ import { first } from 'rxjs/operators'; -import healthCheck from './server/lib/health_check'; import { Cluster } from './server/lib/cluster'; import { createProxy } from './server/lib/create_proxy'; import { handleESError } from './server/lib/handle_es_error'; +import { versionHealthCheck } from './lib/version_health_check'; export default function(kibana) { let defaultVars; @@ -92,15 +92,13 @@ export default function(kibana) { createProxy(server); - // Set up the health check service and start it. - const { start, waitUntilReady } = healthCheck( + const waitUntilHealthy = versionHealthCheck( this, - server, - esConfig.healthCheckDelay.asMilliseconds(), - esConfig.ignoreVersionMismatch + server.logWithMetadata, + server.newPlatform.__internals.elasticsearch.esNodesCompatibility$ ); - server.expose('waitUntilReady', waitUntilReady); - start(); + + server.expose('waitUntilReady', () => waitUntilHealthy); }, }); } diff --git a/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts new file mode 100644 index 0000000000000..5806c31b78414 --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts @@ -0,0 +1,89 @@ +/* + * 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 { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + TestUtils, + createRootWithCorePlugins, + getKbnServer, +} from '../../../../test_utils/kbn_server'; + +import { BehaviorSubject } from 'rxjs'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { NodesVersionCompatibility } from 'src/core/server/elasticsearch/version_check/ensure_es_version'; + +describe('Elasticsearch plugin', () => { + let servers: TestUtils; + let esServer: TestElasticsearchUtils; + let root: TestKibanaUtils['root']; + let elasticsearch: TestKibanaUtils['kbnServer']['server']['plugins']['elasticsearch']; + + const esNodesCompatibility$ = new BehaviorSubject({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + + beforeAll(async function() { + const settings = { + elasticsearch: {}, + adjustTimeout: (t: any) => { + jest.setTimeout(t); + }, + }; + servers = createTestServers(settings); + esServer = await servers.startES(); + + const elasticsearchSettings = { + hosts: esServer.hosts, + username: esServer.username, + password: esServer.password, + }; + root = createRootWithCorePlugins({ elasticsearch: elasticsearchSettings }); + + const setup = await root.setup(); + setup.elasticsearch.esNodesCompatibility$ = esNodesCompatibility$; + await root.start(); + + elasticsearch = getKbnServer(root).server.plugins.elasticsearch; + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }, 30000); + + it("should set it's status to green when all nodes are compatible", done => { + jest.setTimeout(30000); + elasticsearch.status.on('green', () => done()); + }); + + it("should set it's status to red when some nodes aren't compatible", done => { + esNodesCompatibility$.next({ + isCompatible: false, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + elasticsearch.status.on('red', () => done()); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js similarity index 57% rename from src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js rename to src/legacy/core_plugins/elasticsearch/lib/version_health_check.js index 344dbbb5bdf69..4ee8307f490eb 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js +++ b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js @@ -17,11 +17,23 @@ * under the License. */ -import { version as kibanaVersion } from '../../../../../../package.json'; +export const versionHealthCheck = (esPlugin, logWithMetadata, esNodesCompatibility$) => { + esPlugin.status.yellow('Waiting for Elasticsearch'); -export default { - // Make the version stubbable to improve testability. - get() { - return kibanaVersion; - }, + return new Promise(resolve => { + esNodesCompatibility$.subscribe(({ isCompatible, message, kibanaVersion, warningNodes }) => { + if (!isCompatible) { + esPlugin.status.red(message); + } else { + if (message) { + logWithMetadata(['warning'], message, { + kibanaVersion, + nodes: warningNodes, + }); + } + esPlugin.status.green('Ready'); + resolve(); + } + }); + }); }; diff --git a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js new file mode 100644 index 0000000000000..ba7c95bcdfec5 --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js @@ -0,0 +1,71 @@ +/* + * 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 { versionHealthCheck } from './version_health_check'; +import { Subject } from 'rxjs'; + +describe('plugins/elasticsearch', () => { + describe('lib/health_version_check', function() { + let plugin; + let logWithMetadata; + + beforeEach(() => { + plugin = { + status: { + red: jest.fn(), + green: jest.fn(), + yellow: jest.fn(), + }, + }; + + logWithMetadata = jest.fn(); + jest.clearAllMocks(); + }); + + it('returned promise resolves when all nodes are compatible ', function() { + const esNodesCompatibility$ = new Subject(); + const versionHealthyPromise = versionHealthCheck( + plugin, + logWithMetadata, + esNodesCompatibility$ + ); + esNodesCompatibility$.next({ isCompatible: true, message: undefined }); + return expect(versionHealthyPromise).resolves.toBe(undefined); + }); + + it('should set elasticsearch plugin status to green when all nodes are compatible', function() { + const esNodesCompatibility$ = new Subject(); + versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); + expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); + expect(plugin.status.green).not.toHaveBeenCalled(); + esNodesCompatibility$.next({ isCompatible: true, message: undefined }); + expect(plugin.status.green).toHaveBeenCalledWith('Ready'); + expect(plugin.status.red).not.toHaveBeenCalled(); + }); + + it('should set elasticsearch plugin status to red when some nodes are incompatible', function() { + const esNodesCompatibility$ = new Subject(); + versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); + expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); + expect(plugin.status.red).not.toHaveBeenCalled(); + esNodesCompatibility$.next({ isCompatible: false, message: 'your nodes are incompatible' }); + expect(plugin.status.red).toHaveBeenCalledWith('your nodes are incompatible'); + expect(plugin.status.green).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js deleted file mode 100644 index 781d198c66236..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js +++ /dev/null @@ -1,223 +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 _ from 'lodash'; -import Bluebird from 'bluebird'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { esTestConfig } from '@kbn/test'; -import { ensureEsVersion } from '../ensure_es_version'; - -describe('plugins/elasticsearch', () => { - describe('lib/ensure_es_version', () => { - const KIBANA_VERSION = '5.1.0'; - - let server; - - beforeEach(function() { - server = { - log: sinon.stub(), - logWithMetadata: sinon.stub(), - plugins: { - elasticsearch: { - getCluster: sinon - .stub() - .withArgs('admin') - .returns({ callWithInternalUser: sinon.stub() }), - status: { - red: sinon.stub(), - }, - url: esTestConfig.getUrl(), - }, - }, - config() { - return { - get: sinon.stub(), - }; - }, - }; - }); - - function setNodes(/* ...versions */) { - const versions = _.shuffle(arguments); - const nodes = {}; - let i = 0; - - while (versions.length) { - const name = 'node-' + ++i; - const version = versions.shift(); - - const node = { - version: version, - http: { - publish_address: 'http_address', - }, - ip: 'ip', - }; - - if (!_.isString(version)) _.assign(node, version); - nodes[name] = node; - } - - const cluster = server.plugins.elasticsearch.getCluster('admin'); - cluster.callWithInternalUser - .withArgs('nodes.info', sinon.match.any) - .returns(Bluebird.resolve({ nodes: nodes })); - } - - function setNodeWithoutHTTP(version) { - const nodes = { 'node-without-http': { version, ip: 'ip' } }; - const cluster = server.plugins.elasticsearch.getCluster('admin'); - cluster.callWithInternalUser - .withArgs('nodes.info', sinon.match.any) - .returns(Bluebird.resolve({ nodes: nodes })); - } - - it('returns true with single a node that matches', async () => { - setNodes('5.1.0'); - const result = await ensureEsVersion(server, KIBANA_VERSION); - expect(result).to.be(true); - }); - - it('returns true with multiple nodes that satisfy', async () => { - setNodes('5.1.0', '5.2.0', '5.1.1-Beta1'); - const result = await ensureEsVersion(server, KIBANA_VERSION); - expect(result).to.be(true); - }); - - it('throws an error with a single node that is out of date', async () => { - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('does not throw on outdated nodes, if `ignoreVersionMismatch` is enabled in development mode', async () => { - // set config values - server.config = () => ({ - get: name => { - switch (name) { - case 'env.dev': - return true; - default: - throw new Error(`Unknown option "${name}"`); - } - }, - }); - - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - - const ignoreVersionMismatch = true; - const result = await ensureEsVersion(server, KIBANA_VERSION, ignoreVersionMismatch); - expect(result).to.be(true); - }); - - it('throws an error if `ignoreVersionMismatch` is enabled in production mode', async () => { - // set config values - server.config = () => ({ - get: name => { - switch (name) { - case 'env.dev': - return false; - default: - throw new Error(`Unknown option "${name}"`); - } - }, - }); - - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - - try { - const ignoreVersionMismatch = true; - await ensureEsVersion(server, KIBANA_VERSION, ignoreVersionMismatch); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('fails if that single node is a client node', async () => { - setNodes('5.1.0', '5.2.0', { version: '5.0.0', attributes: { client: 'true' } }); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('warns if a node is only off by a patch version', async () => { - setNodes('5.1.1'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - }); - - it('warns if a node is off by a patch version and without http publish address', async () => { - setNodeWithoutHTTP('5.1.1'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - }); - - it('errors if a node incompatible and without http publish address', async () => { - setNodeWithoutHTTP('6.1.1'); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e.message).to.contain('incompatible nodes'); - expect(e).to.be.a(Error); - } - }); - - it('only warns once per node list', async () => { - setNodes('5.1.1'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 3); - expect(server.logWithMetadata.getCall(2).args[0]).to.contain('debug'); - }); - - it('warns again if the node list changes', async () => { - setNodes('5.1.1'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - - setNodes('5.1.2'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 4); - expect(server.logWithMetadata.getCall(2).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(3).args[0]).to.contain('warning'); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js deleted file mode 100644 index 3b593c6352394..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js +++ /dev/null @@ -1,151 +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 Bluebird from 'bluebird'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -const NoConnections = require('elasticsearch').errors.NoConnections; - -import healthCheck from '../health_check'; -import kibanaVersion from '../kibana_version'; - -const esPort = 9220; - -describe('plugins/elasticsearch', () => { - describe('lib/health_check', function() { - let health; - let plugin; - let cluster; - let server; - const sandbox = sinon.createSandbox(); - - function getTimerCount() { - return Object.keys(sandbox.clock.timers || {}).length; - } - - beforeEach(() => { - sandbox.useFakeTimers(); - const COMPATIBLE_VERSION_NUMBER = '5.0.0'; - - // Stub the Kibana version instead of drawing from package.json. - sandbox.stub(kibanaVersion, 'get').returns(COMPATIBLE_VERSION_NUMBER); - - // setup the plugin stub - plugin = { - name: 'elasticsearch', - status: { - red: sinon.stub(), - green: sinon.stub(), - yellow: sinon.stub(), - }, - }; - - cluster = { callWithInternalUser: sinon.stub(), errors: { NoConnections } }; - cluster.callWithInternalUser.withArgs('index', sinon.match.any).returns(Bluebird.resolve()); - cluster.callWithInternalUser - .withArgs('mget', sinon.match.any) - .returns(Bluebird.resolve({ ok: true })); - cluster.callWithInternalUser - .withArgs('get', sinon.match.any) - .returns(Bluebird.resolve({ found: false })); - cluster.callWithInternalUser - .withArgs('search', sinon.match.any) - .returns(Bluebird.resolve({ hits: { hits: [] } })); - cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any).returns( - Bluebird.resolve({ - nodes: { - 'node-01': { - version: COMPATIBLE_VERSION_NUMBER, - http_address: `inet[/127.0.0.1:${esPort}]`, - ip: '127.0.0.1', - }, - }, - }) - ); - - // Setup the server mock - server = { - logWithMetadata: sinon.stub(), - info: { port: 5601 }, - config: () => ({ get: sinon.stub() }), - plugins: { - elasticsearch: { - getCluster: sinon.stub().returns(cluster), - }, - }, - ext: sinon.stub(), - }; - - health = healthCheck(plugin, server, 0); - }); - - afterEach(() => sandbox.restore()); - - it('should stop when cluster is shutdown', () => { - // ensure that health.start() is responsible for the timer we are observing - expect(getTimerCount()).to.be(0); - health.start(); - expect(getTimerCount()).to.be(1); - - // ensure that a server extension was registered - sinon.assert.calledOnce(server.ext); - sinon.assert.calledWithExactly(server.ext, sinon.match.string, sinon.match.func); - - const [, handler] = server.ext.firstCall.args; - handler(); // this should be health.stop - - // ensure that the handler unregistered the timer - expect(getTimerCount()).to.be(0); - }); - - it('should set the cluster green if everything is ready', function() { - cluster.callWithInternalUser.withArgs('ping').returns(Bluebird.resolve()); - - return health.run().then(function() { - sinon.assert.calledOnce(plugin.status.yellow); - sinon.assert.calledWithExactly(plugin.status.yellow, 'Waiting for Elasticsearch'); - - sinon.assert.calledOnce( - cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any) - ); - sinon.assert.notCalled(plugin.status.red); - sinon.assert.calledOnce(plugin.status.green); - sinon.assert.calledWithExactly(plugin.status.green, 'Ready'); - }); - }); - - describe('#waitUntilReady', function() { - it('waits for green status', function() { - plugin.status.once = sinon.spy(function(event, handler) { - expect(event).to.be('green'); - setImmediate(handler); - }); - - const waitUntilReadyPromise = health.waitUntilReady(); - - sandbox.clock.runAll(); - - return waitUntilReadyPromise.then(function() { - sinon.assert.calledOnce(plugin.status.once); - }); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js b/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js deleted file mode 100644 index 8d304cd558418..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js +++ /dev/null @@ -1,126 +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. - */ - -/** - * ES and Kibana versions are locked, so Kibana should require that ES has the same version as - * that defined in Kibana's package.json. - */ - -import { forEach, get } from 'lodash'; -import { coerce } from 'semver'; -import isEsCompatibleWithKibana from './is_es_compatible_with_kibana'; - -/** - * tracks the node descriptions that get logged in warnings so - * that we don't spam the log with the same message over and over. - * - * There are situations, like in testing or multi-tenancy, where - * the server argument changes, so we must track the previous - * node warnings per server - */ -const lastWarnedNodesForServer = new WeakMap(); - -export function ensureEsVersion(server, kibanaVersion, ignoreVersionMismatch = false) { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - - server.logWithMetadata(['plugin', 'debug'], 'Checking Elasticsearch version'); - return callWithInternalUser('nodes.info', { - filterPath: ['nodes.*.version', 'nodes.*.http.publish_address', 'nodes.*.ip'], - }).then(function(info) { - // Aggregate incompatible ES nodes. - const incompatibleNodes = []; - - // Aggregate ES nodes which should prompt a Kibana upgrade. - const warningNodes = []; - - forEach(info.nodes, esNode => { - if (!isEsCompatibleWithKibana(esNode.version, kibanaVersion)) { - // Exit early to avoid collecting ES nodes with newer major versions in the `warningNodes`. - return incompatibleNodes.push(esNode); - } - - // It's acceptable if ES and Kibana versions are not the same so long as - // they are not incompatible, but we should warn about it - - // Ignore version qualifiers - // https://github.com/elastic/elasticsearch/issues/36859 - const looseMismatch = coerce(esNode.version).version !== coerce(kibanaVersion).version; - if (looseMismatch) { - warningNodes.push(esNode); - } - }); - - function getHumanizedNodeNames(nodes) { - return nodes.map(node => { - const publishAddress = get(node, 'http.publish_address') - ? get(node, 'http.publish_address') + ' ' - : ''; - return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; - }); - } - - if (warningNodes.length) { - const simplifiedNodes = warningNodes.map(node => ({ - version: node.version, - http: { - publish_address: get(node, 'http.publish_address'), - }, - ip: node.ip, - })); - - // Don't show the same warning over and over again. - const warningNodeNames = getHumanizedNodeNames(simplifiedNodes).join(', '); - if (lastWarnedNodesForServer.get(server) !== warningNodeNames) { - lastWarnedNodesForServer.set(server, warningNodeNames); - server.logWithMetadata( - ['warning'], - `You're running Kibana ${kibanaVersion} with some different versions of ` + - 'Elasticsearch. Update Kibana or Elasticsearch to the same ' + - `version to prevent compatibility issues: ${warningNodeNames}`, - { - kibanaVersion, - nodes: simplifiedNodes, - } - ); - } - } - - if (incompatibleNodes.length && !shouldIgnoreVersionMismatch(server, ignoreVersionMismatch)) { - const incompatibleNodeNames = getHumanizedNodeNames(incompatibleNodes); - throw new Error( - `This version of Kibana requires Elasticsearch v` + - `${kibanaVersion} on all nodes. I found ` + - `the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(', ')}` - ); - } - - return true; - }); -} - -function shouldIgnoreVersionMismatch(server, ignoreVersionMismatch) { - const isDevMode = server.config().get('env.dev'); - if (!isDevMode && ignoreVersionMismatch) { - throw new Error( - `Option "elasticsearch.ignoreVersionMismatch" can only be used in development mode` - ); - } - - return isDevMode && ignoreVersionMismatch; -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js b/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js deleted file mode 100644 index 40053ec774542..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js +++ /dev/null @@ -1,75 +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 Bluebird from 'bluebird'; -import kibanaVersion from './kibana_version'; -import { ensureEsVersion } from './ensure_es_version'; - -export default function(plugin, server, requestDelay, ignoreVersionMismatch) { - plugin.status.yellow('Waiting for Elasticsearch'); - - function waitUntilReady() { - return new Bluebird(resolve => { - plugin.status.once('green', resolve); - }); - } - - function check() { - return ensureEsVersion(server, kibanaVersion.get(), ignoreVersionMismatch) - .then(() => plugin.status.green('Ready')) - .catch(err => plugin.status.red(err)); - } - - let timeoutId = null; - - function scheduleCheck(ms) { - if (timeoutId) return; - - const myId = setTimeout(function() { - check().finally(function() { - if (timeoutId === myId) startorRestartChecking(); - }); - }, ms); - - timeoutId = myId; - } - - function startorRestartChecking() { - scheduleCheck(stopChecking() ? requestDelay : 1); - } - - function stopChecking() { - if (!timeoutId) return false; - clearTimeout(timeoutId); - timeoutId = null; - return true; - } - - server.ext('onPreStop', stopChecking); - - return { - waitUntilReady: waitUntilReady, - run: check, - start: startorRestartChecking, - stop: stopChecking, - isRunning: function() { - return !!timeoutId; - }, - }; -} diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts index 4898cb2b67852..d91438d904558 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -29,7 +29,7 @@ let mockDefaultRouteSetting: any = ''; describe('default route provider', () => { let root: Root; beforeAll(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); await root.setup(); await root.start(); diff --git a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts index da785a59893ab..8365941cbeb10 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts @@ -30,6 +30,7 @@ describe('default route provider', () => { server: { defaultRoute: '/app/some/default/route', }, + migrations: { skip: true }, }); await root.setup(); diff --git a/src/legacy/server/http/integration_tests/max_payload_size.test.js b/src/legacy/server/http/integration_tests/max_payload_size.test.js index 4408f0141bb21..7f22f83c78f0e 100644 --- a/src/legacy/server/http/integration_tests/max_payload_size.test.js +++ b/src/legacy/server/http/integration_tests/max_payload_size.test.js @@ -21,7 +21,7 @@ import * as kbnTestServer from '../../../../test_utils/kbn_server'; let root; beforeAll(async () => { - root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 } }); + root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 }, migrations: { skip: true } }); await root.setup(); await root.start(); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 776275715921b..92be88b91c652 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -108,7 +108,10 @@ describe('onPostAuthInterceptor', () => { availableSpaces: any[], testOptions = { simulateGetSpacesFailure: false, simulateGetSingleSpaceFailure: false } ) { - const { http } = await root.setup(); + const { http, elasticsearch } = await root.setup(); + + // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check + elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; const loggingMock = loggingServiceMock .create() diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index d6ff4a20052e4..5e6cf67ee8c90 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -17,6 +17,7 @@ import { import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; import { LegacyAPI } from '../../plugin'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('onRequestInterceptor', () => { let root: ReturnType; @@ -104,7 +105,9 @@ describe('onRequestInterceptor', () => { routes: 'legacy' | 'new-platform'; } async function setup(opts: SetupOpts = { basePath: '/', routes: 'legacy' }) { - const { http } = await root.setup(); + const { http, elasticsearch } = await root.setup(); + // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check + elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; initSpacesOnRequestInterceptor({ getLegacyAPI: () => From 2260e7cdbe2da24bfa13866fc37bcdbc8793dd74 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 31 Jan 2020 18:10:34 +0100 Subject: [PATCH 3/8] [Uptime] Added location overview in monitor in status column (#55720) * Reintroduce a column for url. * Reintroduce original URL column. * added location overview column * fix types * update snaps * Update busted test snapshots. * Update x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx Co-Authored-By: Justin Kambic * update test * rename * update mobile layout * update column laytout * fixed i18n * remove icon Co-authored-by: Justin Kambic Co-authored-by: Elastic Machine --- .../uptime/common/constants/constants.ts | 7 - .../plugins/uptime/common/constants/index.ts | 2 +- .../plugins/uptime/common/constants/ui.ts | 33 +++ .../monitor_bar_series.test.tsx.snap | 1 + .../functional/charts/monitor_bar_series.tsx | 2 +- .../location_map/location_status_tags.tsx | 24 +- .../__snapshots__/monitor_list.test.tsx.snap | 26 +- .../monitor_list_pagination.test.tsx.snap | 24 +- .../monitor_list_status_column.test.tsx.snap | 239 ++++++++++++++--- .../monitor_list_status_column.test.tsx | 241 +++++++++++++++++- .../functional/monitor_list/monitor_list.tsx | 31 ++- .../monitor_status_list.tsx | 13 +- .../monitor_status_row.tsx | 7 +- .../monitor_list_status_column.tsx | 127 +++++++-- .../functional/monitor_list/translations.ts | 8 + 15 files changed, 662 insertions(+), 123 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/common/constants/constants.ts create mode 100644 x-pack/legacy/plugins/uptime/common/constants/ui.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/constants.ts b/x-pack/legacy/plugins/uptime/common/constants/constants.ts deleted file mode 100644 index 0c35bc9734486..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/constants/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const UNNAMED_LOCATION = 'Unnamed-location'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 0a95960825f02..9d5ad4607491c 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -11,4 +11,4 @@ export { INDEX_NAMES } from './index_names'; export * from './capabilities'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; -export * from './constants'; +export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/legacy/plugins/uptime/common/constants/ui.ts new file mode 100644 index 0000000000000..c91a2f6625194 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/constants/ui.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum STATUS { + UP = 'up', + DOWN = 'down', +} + +export const UNNAMED_LOCATION = 'Unnamed-location'; + +export const SHORT_TS_LOCALE = 'en-short-locale'; + +export const SHORT_TIMESPAN_LOCALE = { + relativeTime: { + future: 'in %s', + past: '%s ago', + s: '%ds', + ss: '%ss', + m: '%dm', + mm: '%dm', + h: '%dh', + hh: '%dh', + d: '%dd', + dd: '%dd', + M: '%d Mon', + MM: '%d Mon', + y: '%d Yr', + yy: '%d Yr', + }, +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index 78b2bfdecb87a..c3b99c9785cbe 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -5,6 +5,7 @@ exports[`MonitorBarSeries component renders a series when there are down items 1 style={ Object { "height": 50, + "marginRight": 15, "maxWidth": "1200px", "width": "100%", } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index a0cbdc5922123..ce91bf5b1638f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -53,7 +53,7 @@ export const MonitorBarSeries = ({ const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? ( -
+
{ const prevLocal: string = moment.locale() ?? 'en'; const renderTags = () => { - moment.defineLocale('en-tag', { - relativeTime: { - future: 'in %s', - past: '%s ago', - s: '%ds', - ss: '%ss', - m: '%dm', - mm: '%dm', - h: '%dh', - hh: '%dh', - d: '%dd', - dd: '%dd', - M: '%d Mon', - MM: '%d Mon', - y: '%d Yr', - yy: '%d Yr', - }, - }); + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + const tags = ( {downLocations.map((item, ind) => tagLabel(item, ind, danger))} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 1de49f1223699..f779efca7b18a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -24,20 +24,24 @@ exports[`MonitorList component renders a no items message when no data is provid Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -58,6 +62,7 @@ exports[`MonitorList component renders a no items message when no data is provid "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -83,6 +88,7 @@ exports[`MonitorList component renders a no items message when no data is provid @@ -122,20 +129,24 @@ exports[`MonitorList component renders the monitor list 1`] = ` Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -156,6 +167,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -243,6 +255,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap index aa9d3a3ed0d8c..03a5a15eea1a4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap @@ -24,20 +24,24 @@ exports[`MonitorList component renders a no items message when no data is provid Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -58,6 +62,7 @@ exports[`MonitorList component renders a no items message when no data is provid "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -83,6 +88,7 @@ exports[`MonitorList component renders a no items message when no data is provid @@ -122,20 +129,24 @@ exports[`MonitorList component renders the monitor list 1`] = ` Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -156,6 +167,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap index 69a0616a7bed8..f3b90fbf6ee91 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap @@ -1,11 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MonitorListStatusColumn can handle a non-numeric timestamp value 1`] = ` - - + Up - + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" + > - Thu May 09 2019 10:15:11 GMT-0400 + a few seconds ago - } - delay="regular" - position="top" + + + + + - - a few seconds ago - - + in 0/0 Location + - + `; exports[`MonitorListStatusColumn provides expected tooltip and display times 1`] = ` - - + Up - + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" + > - Thu May 09 2019 10:15:11 GMT-0400 + a few seconds ago + + + + + + in 0/0 Location + + + +`; + +exports[`MonitorListStatusColumn will display location status 1`] = ` + + + - + + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" > - a few seconds ago - - + + a few seconds ago + + + + + + + in 1/3 Locations + - + +`; + +exports[`MonitorListStatusColumn will render display location status 1`] = ` +.c1 { + padding-left: 17px; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +
+
+
+
+
+ +
+
+ Up +
+
+
+ + +
+
+ a few seconds ago +
+
+
+
+
+
+
+ in 1/3 Locations +
+
+
`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx index 2a834377fee8e..406e18535f34c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx @@ -6,8 +6,10 @@ import React from 'react'; import moment from 'moment'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { MonitorListStatusColumn } from '../monitor_list_status_column'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { getLocationStatus, MonitorListStatusColumn } from '../monitor_list_status_column'; +import { Check } from '../../../../../common/graphql/types'; +import { STATUS } from '../../../../../common/constants'; describe('MonitorListStatusColumn', () => { beforeAll(() => { @@ -18,15 +20,246 @@ describe('MonitorListStatusColumn', () => { Date.prototype.toString = jest.fn(() => 'Tue, 01 Jan 2019 00:00:00 GMT'); }); + let upChecks: Check[]; + + let downChecks: Check[]; + + let checks: Check[]; + + beforeEach(() => { + upChecks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + + downChecks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + + checks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + }); + it('provides expected tooltip and display times', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl( + + ); expect(component).toMatchSnapshot(); }); it('can handle a non-numeric timestamp value', () => { const component = shallowWithIntl( - + + ); + expect(component).toMatchSnapshot(); + }); + + it('will display location status', () => { + const component = shallowWithIntl( + ); expect(component).toMatchSnapshot(); }); + + it('will render display location status', () => { + const component = renderWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it(' will test getLocationStatus location', () => { + let statusMessage = getLocationStatus(checks, STATUS.UP); + + expect(statusMessage).toBe('in 1/3 Locations'); + + statusMessage = getLocationStatus(checks, STATUS.DOWN); + + expect(statusMessage).toBe('in 2/3 Locations'); + + statusMessage = getLocationStatus(upChecks, STATUS.UP); + + expect(statusMessage).toBe('in 3/3 Locations'); + + statusMessage = getLocationStatus(downChecks, STATUS.UP); + + expect(statusMessage).toBe('in 0/3 Locations'); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index 1d0930f1faaef..c8385440a7d49 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -16,8 +16,7 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { withUptimeGraphQL, UptimeGraphQLQueryProps } from '../../higher_order'; import { monitorStatesQuery } from '../../../queries/monitor_states_query'; @@ -68,10 +67,11 @@ export const MonitorListComponent = (props: Props) => { loading, } = props; const [drawerIds, updateDrawerIds] = useState([]); - const items = get(data, 'monitorStates.summaries', []); - const nextPagePagination = get(data, 'monitorStates.nextPagePagination'); - const prevPagePagination = get(data, 'monitorStates.prevPagePagination'); + const items = data?.monitorStates?.summaries ?? []; + + const nextPagePagination = data?.monitorStates?.nextPagePagination ?? ''; + const prevPagePagination = data?.monitorStates?.prevPagePagination ?? ''; const getExpandedRowMap = () => { return drawerIds.reduce((map: ExpandedRowMap, id: string) => { @@ -89,18 +89,24 @@ export const MonitorListComponent = (props: Props) => { const columns = [ { align: 'left' as const, - width: '20%', field: 'state.monitor.status', name: labels.STATUS_COLUMN_LABEL, - render: (status: string, { state: { timestamp } }: MonitorSummary) => { - return ; + mobileOptions: { + fullWidth: true, + }, + render: (status: string, { state: { timestamp, checks } }: MonitorSummary) => { + return ( + + ); }, }, { align: 'left' as const, - width: '30%', field: 'state.monitor.name', name: labels.NAME_COLUMN_LABEL, + mobileOptions: { + fullWidth: true, + }, render: (name: string, summary: MonitorSummary) => ( {name ? name : `Unnamed - ${summary.monitor_id}`} @@ -109,7 +115,7 @@ export const MonitorListComponent = (props: Props) => { sortable: true, }, { - aligh: 'left' as const, + align: 'left' as const, field: 'state.url.full', name: labels.URL, render: (url: string, summary: MonitorSummary) => ( @@ -140,6 +146,7 @@ export const MonitorListComponent = (props: Props) => { name: '', sortable: true, isExpander: true, + width: '24px', render: (id: string) => { return ( { ]; return ( - + <>
@@ -211,7 +218,7 @@ export const MonitorListComponent = (props: Props) => { - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx index 04c5dc7d71371..a2042e379dd80 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Check } from '../../../../../common/graphql/types'; import { LocationLink } from './location_link'; import { MonitorStatusRow } from './monitor_status_row'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { STATUS, UNNAMED_LOCATION } from '../../../../../common/constants'; interface MonitorStatusListProps { /** @@ -20,9 +20,6 @@ interface MonitorStatusListProps { checks: Check[]; } -export const UP = 'up'; -export const DOWN = 'down'; - export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { const upChecks: Set = new Set(); const downChecks: Set = new Set(); @@ -31,9 +28,9 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { // Doing this way because name is either string or null, get() default value only works on undefined value const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; - if (check.monitor.status === UP) { + if (check.monitor.status === STATUS.UP) { upChecks.add(capitalize(location)); - } else if (check.monitor.status === DOWN) { + } else if (check.monitor.status === STATUS.DOWN) { downChecks.add(capitalize(location)); } }); @@ -43,8 +40,8 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { return ( <> - - + + {(downChecks.has(UNNAMED_LOCATION) || upChecks.has(UNNAMED_LOCATION)) && ( <> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx index e724986c2505e..50028e1ddea18 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx @@ -8,8 +8,7 @@ import React, { useContext } from 'react'; import { EuiHealth, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { UptimeThemeContext } from '../../../../contexts'; -import { UP } from './monitor_status_list'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { UNNAMED_LOCATION, STATUS } from '../../../../../common/constants'; interface MonitorStatusRowProps { /** @@ -27,7 +26,7 @@ export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProp colors: { success, danger }, } = useContext(UptimeThemeContext); - const color = status === UP ? success : danger; + const color = status === STATUS.UP ? success : danger; let checkListArray = [...locationNames]; // If un-named location exists, move it to end @@ -44,7 +43,7 @@ export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProp return ( <> - {status === UP ? ( + {status === STATUS.UP ? ( { switch (status) { - case 'up': + case STATUS.UP: return 'success'; - case 'down': + case STATUS.DOWN: return 'danger'; default: return ''; @@ -27,42 +50,98 @@ const getHealthColor = (status: string): string => { const getHealthMessage = (status: string): string | null => { switch (status) { - case 'up': - return i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { - defaultMessage: 'Up', - }); - case 'down': - return i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { - defaultMessage: 'Down', - }); + case STATUS.UP: + return labels.UP; + case STATUS.DOWN: + return labels.DOWN; default: return null; } }; +const getRelativeShortTimeStamp = (timeStamp: any) => { + const prevLocale: string = moment.locale() ?? 'en'; + + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + + const shortTimestamp = parseTimestamp(timeStamp).fromNow(); + + // Reset it so, it does't impact other part of the app + moment.locale(prevLocale); + return shortTimestamp; +}; + +export const getLocationStatus = (checks: Check[], status: string) => { + const upChecks: Set = new Set(); + const downChecks: Set = new Set(); + + checks.forEach((check: Check) => { + const location = check?.observer?.geo?.name ?? UNNAMED_LOCATION; + + if (check.monitor.status === STATUS.UP) { + upChecks.add(capitalize(location)); + } else if (check.monitor.status === STATUS.DOWN) { + downChecks.add(capitalize(location)); + } + }); + + // if monitor is down in one dns, it will be considered down so removing it from up list + const absUpChecks: Set = new Set([...upChecks].filter(item => !downChecks.has(item))); + + const totalLocations = absUpChecks.size + downChecks.size; + let statusMessage = ''; + if (status === STATUS.DOWN) { + statusMessage = `${downChecks.size}/${totalLocations}`; + } else { + statusMessage = `${absUpChecks.size}/${totalLocations}`; + } + + if (totalLocations > 1) { + return i18n.translate('xpack.uptime.monitorList.statusColumn.locStatusMessage.multiple', { + defaultMessage: 'in {noLoc} Locations', + values: { noLoc: statusMessage }, + }); + } + + return i18n.translate('xpack.uptime.monitorList.statusColumn.locStatusMessage', { + defaultMessage: 'in {noLoc} Location', + values: { noLoc: statusMessage }, + }); +}; + export const MonitorListStatusColumn = ({ status, + checks = [], timestamp: tsString, }: MonitorListStatusColumnProps) => { const timestamp = parseTimestamp(tsString); return ( - - + + {getHealthMessage(status)} - - {timestamp.toLocaleString()} + + + {timestamp.toLocaleString()} + + } + > + + {getRelativeShortTimeStamp(tsString)} - } - > - - {timestamp.fromNow()} - - + + + + + {getLocationStatus(checks, status)} - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts index beacdec1ae265..5252d90215e95 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts @@ -56,3 +56,11 @@ export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMe export const URL = i18n.translate('xpack.uptime.monitorList.table.url.name', { defaultMessage: 'Url', }); + +export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { + defaultMessage: 'Up', +}); + +export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { + defaultMessage: 'Down', +}); From 440a37a6dbc13427755e027418ee5e3aed482295 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Fri, 31 Jan 2020 11:11:07 -0600 Subject: [PATCH 4/8] [DOCS] Share options for Visualize (#56407) * [DOCS] Share options for Visualize * Removed files * Comments from Gail --- docs/user/visualize.asciidoc | 51 ++++++++++++++++++++++++++++--- docs/visualize/inspector.asciidoc | 11 ------- docs/visualize/saving.asciidoc | 19 ------------ 3 files changed, 47 insertions(+), 34 deletions(-) delete mode 100644 docs/visualize/inspector.asciidoc delete mode 100644 docs/visualize/saving.asciidoc diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index cfd2bac4989c1..5692fe6d1ae01 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -75,6 +75,53 @@ modifications to the saved search are automatically reflected in the visualization. To disable automatic updates, you can disconnect a visualization from the saved search. +[float] +[[vis-inspector]] +== Inspect visualizations + +Many visualizations allow you to inspect the query and data behind the visualization. + +. In the {kib} toolbar, click *Inspect*. +. To download the data, click *Download CSV*, then choose one of the following options: +* *Formatted CSV* - Downloads the data in table format. +* *Raw CSV* - Downloads the data as provided. +. To view the requests for collecting data, select *Requests* from the *View* +dropdown. + +[float] +[[save-visualize]] +== Save visualizations +To use your visualizations in <>, you must save them. + +. In the {kib} toolbar, click *Save*. +. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. + +To access the saved visualization, go to *Management > {kib} > Saved Objects*. + +[float] +[[save-visualization-read-only-access]] +==== Read only access +When you have insufficient privileges to save visualizations, the following indicator is +displayed and the *Save* button is not visible. + +For more information, refer to <>. + +[role="screenshot"] +image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] + +[float] +[[visualize-share-options]] +== Share visualizations + +When you've finished your visualization, you can share it outside of {kib}. + +From the *Share* menu, you can: + +* Embed the code in a web page. Users must have {kib} access +to view an embedded visualization. +* Share a direct link to a {kib} visualization. +* Generate a PDF report. +* Generate a PNG report. -- include::{kib-repo-dir}/visualize/visualize_rollup_data.asciidoc[] @@ -95,7 +142,3 @@ include::{kib-repo-dir}/visualize/heatmap.asciidoc[] include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] include::{kib-repo-dir}/visualize/vega.asciidoc[] - -include::{kib-repo-dir}/visualize/saving.asciidoc[] - -include::{kib-repo-dir}/visualize/inspector.asciidoc[] diff --git a/docs/visualize/inspector.asciidoc b/docs/visualize/inspector.asciidoc deleted file mode 100644 index ed98daea211e1..0000000000000 --- a/docs/visualize/inspector.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[[vis-inspector]] -== Inspect visualizations - -Many visualizations allow you to inspect the query and data behind the visualization. - -. In the {kib} toolbar, click *Inspect*. -. To download the data, click *Download CSV*, then choose one of the following options: -* *Formatted CSV* - Downloads the data in table format. -* *Raw CSV* - Downloads the data as provided. -. To view the data collection requests, select *Requests* from the *View* -dropdown. diff --git a/docs/visualize/saving.asciidoc b/docs/visualize/saving.asciidoc deleted file mode 100644 index e3330446bfad1..0000000000000 --- a/docs/visualize/saving.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[[save-visualize]] -== Save visualizations -To use your visualizations in <>, you must save them. - -. In the {kib} toolbar, click *Save*. -. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. - -To access the saved visualization, go to *Management > {kib} > Saved Objects*. - -[float] -[[save-visualization-read-only-access]] -==== Read only access -When you have insufficient privileges to save visualizations, the following indicator is -displayed and the *Save* button is not visible. - -[role="screenshot"] -image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] - -For more information, see <>. From e24863ed86bc71aeff5eadadcdc916cd5ed99502 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 31 Jan 2020 12:30:19 -0500 Subject: [PATCH 5/8] [SIEM][Detection Engine] Modified gap detection util to accept all dateMath formats (#56055) * Partial commit - got tests working and `getGapBetweenRuns` working with `from` and `to` being any string (invalid, ISO string, now, now-x). Co-authored-by: Elastic Machine --- .../detection_engine/signals/utils.test.ts | 136 +++++++++++++----- .../lib/detection_engine/signals/utils.ts | 30 ++-- 2 files changed, 123 insertions(+), 43 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts index d6a3da5a393f8..bf25ab8bfd7ea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -5,14 +5,29 @@ */ import moment from 'moment'; +import sinon from 'sinon'; -import { generateId, parseInterval, getDriftTolerance, getGapBetweenRuns } from './utils'; +import { + generateId, + parseInterval, + parseScheduleDates, + getDriftTolerance, + getGapBetweenRuns, +} from './utils'; describe('utils', () => { + const anchor = '2020-01-01T06:06:06.666Z'; + const unix = moment(anchor).valueOf(); let nowDate = moment('2020-01-01T00:00:00.000Z'); + let clock: sinon.SinonFakeTimers; beforeEach(() => { nowDate = moment('2020-01-01T00:00:00.000Z'); + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); }); describe('generateId', () => { @@ -27,7 +42,7 @@ describe('utils', () => { }); }); - describe('getIntervalMilliseconds', () => { + describe('parseInterval', () => { test('it returns a duration when given one that is valid', () => { const duration = parseInterval('5m'); expect(duration).not.toBeNull(); @@ -40,8 +55,36 @@ describe('utils', () => { }); }); - describe('getDriftToleranceMilliseconds', () => { - test('it returns a drift tolerance in milliseconds of 1 minute when from overlaps to by 1 minute and the interval is 5 minutes', () => { + describe('parseScheduleDates', () => { + test('it returns a moment when given an ISO string', () => { + const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); + expect(result).not.toBeNull(); + expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); + }); + + test('it returns a moment when given `now`', () => { + const result = parseScheduleDates('now'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns a moment when given `now-x`', () => { + const result = parseScheduleDates('now-6m'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { + const result = parseScheduleDates('invalid'); + + expect(result).toBeNull(); + }); + }); + + describe('getDriftTolerance', () => { + test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => { const drift = getDriftTolerance({ from: 'now-6m', to: 'now', @@ -51,7 +94,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns a drift tolerance of 0 when from equals the interval', () => { + test('it returns a drift tolerance of 0 when "from" equals the interval', () => { const drift = getDriftTolerance({ from: 'now-5m', to: 'now', @@ -60,7 +103,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(0); }); - test('it returns a drift tolerance of 5 minutes when from is 10 minutes but the interval is 5 minutes', () => { + test('it returns a drift tolerance of 5 minutes when "from" is 10 minutes but the interval is 5 minutes', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', @@ -70,7 +113,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); }); - test('it returns a drift tolerance of 10 minutes when from is 10 minutes ago and the interval is 0', () => { + test('it returns a drift tolerance of 10 minutes when "from" is 10 minutes ago and the interval is 0', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', @@ -80,36 +123,61 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); }); - test('returns null if the "to" is not "now" since we have limited support for date math', () => { + test('returns a drift tolerance of 1 minute when "from" is invalid and defaults to "now-6m" and interval is 5 minutes', () => { const drift = getDriftTolerance({ - from: 'now-6m', - to: 'invalid', // if not set to "now" this function returns null - interval: moment.duration(1000, 'milliseconds'), + from: 'invalid', + to: 'now', + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('returns null if the "from" does not start with "now-" since we have limited support for date math', () => { + test('returns a drift tolerance of 1 minute when "from" does not include `now` and defaults to "now-6m" and interval is 5 minutes', () => { const drift = getDriftTolerance({ - from: 'valid', // if not set to "now-x" where x is an interval such as 6m + from: '10m', to: 'now', - interval: moment.duration(1000, 'milliseconds'), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('returns a drift tolerance of 4 minutes when "to" is "now-x", from is a valid input and interval is 5 minute', () => { + const drift = getDriftTolerance({ + from: 'now-10m', + to: 'now-1m', + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); }); - test('returns null if the "from" starts with "now-" but has a string instead of an integer', () => { + test('it returns expected drift tolerance when "from" is an ISO string', () => { const drift = getDriftTolerance({ - from: 'now-dfdf', // if not set to "now-x" where x is an interval such as 6m + from: moment() + .subtract(10, 'minutes') + .toISOString(), to: 'now', - interval: moment.duration(1000, 'milliseconds'), + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); + }); + + test('it returns expected drift tolerance when "to" is an ISO string', () => { + const drift = getDriftTolerance({ + from: 'now-6m', + to: moment().toISOString(), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); describe('getGapBetweenRuns', () => { - test('it returns a gap of 0 when from and interval match each other and the previous started was from the previous interval time', () => { + test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -121,7 +189,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(0); }); - test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -133,7 +201,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); }); - test('it returns a negative gap of 5 minutes when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -145,7 +213,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(-5, 'minute').asMilliseconds()); }); - test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(10, 'minutes'), interval: '10m', @@ -233,26 +301,28 @@ describe('utils', () => { expect(gap).toBeNull(); }); - test('it returns null if from is an invalid string such as "invalid"', () => { + test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().subtract(7, 'minutes'), interval: '5m', - from: 'invalid', // if not set to "now-x" where x is an interval such as 6m + from: 'invalid', to: 'now', now: nowDate.clone(), }); - expect(gap).toBeNull(); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns null if to is an invalid string such as "invalid"', () => { + test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().subtract(7, 'minutes'), interval: '5m', - from: 'now-5m', - to: 'invalid', // if not set to "now" this function returns null + from: 'now-6m', + to: 'invalid', now: nowDate.clone(), }); - expect(gap).toBeNull(); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts index 5a4c67ebaaa36..940ea8be2ac36 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -5,6 +5,7 @@ */ import { createHash } from 'crypto'; import moment from 'moment'; +import dateMath from '@elastic/datemath'; import { parseDuration } from '../../../../../alerting/server/lib'; @@ -26,25 +27,34 @@ export const parseInterval = (intervalString: string): moment.Duration | null => } }; +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; + export const getDriftTolerance = ({ from, to, interval, + now = moment(), }: { from: string; to: string; interval: moment.Duration; + now?: moment.Moment; }): moment.Duration | null => { - if (to.trim() !== 'now') { - // we only support 'now' for drift detection - return null; - } - if (!from.trim().startsWith('now-')) { - // we only support from tha starts with now for drift detection - return null; - } - const split = from.split('-'); - const duration = parseInterval(split[1]); + const toDate = parseScheduleDates(to) ?? now; + const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); + const timeSegment = toDate.diff(fromDate); + const duration = moment.duration(timeSegment); + if (duration !== null) { return duration.subtract(interval); } else { From 74ee0d0ba41c4dc60ad2595f85b5a6f4be1696ec Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 31 Jan 2020 14:26:12 -0500 Subject: [PATCH 6/8] [Docs] - Deprecate kibana_user role (#54606) * rename kibana_user => kibana_admin * add migration note * [DOCS] Fixes link for use in multiple books * i18n fixes after merge from master * fix i18n * Apply suggestions from code review Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Apply suggestions from code review Co-Authored-By: Brandon Kobel Co-authored-by: Lisa Cawley Co-authored-by: Elastic Machine Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Brandon Kobel --- docs/management/watcher-ui/index.asciidoc | 2 +- docs/migration/migrate_8_0.asciidoc | 15 ++++++++++++ docs/uptime-guide/security.asciidoc | 4 ++-- docs/user/monitoring/viewing-metrics.asciidoc | 2 +- .../security/authorization/index.asciidoc | 6 ++--- docs/user/security/reporting.asciidoc | 8 +++---- docs/user/security/securing-kibana.asciidoc | 4 ++-- .../public/views/access_denied/index.html | 4 ++-- .../utils/beat_schema/8.0.0/filebeat.ts | 2 +- .../plugins/transform/common/constants.ts | 4 ++-- ...pace_aware_privilege_section.test.tsx.snap | 8 +++---- .../space_aware_privilege_section.tsx | 8 +++---- .../translations/translations/ja-JP.json | 6 ++--- .../translations/translations/zh-CN.json | 6 ++--- .../apis/console/feature_controls.ts | 23 +++++++++++++++++++ .../monitoring/setup/collection/security.js | 2 +- .../apis/short_urls/feature_controls.ts | 4 ++-- .../dashboard_mode/dashboard_view_mode.js | 2 +- .../apps/security/doc_level_security_roles.js | 4 ++-- .../apps/security/field_level_security.js | 8 +++---- .../apps/security/secure_roles_perm.js | 4 ++-- .../functional/apps/security/user_email.js | 4 ++-- x-pack/test/functional/apps/security/users.js | 6 ++--- .../page_objects/monitoring_page.js | 2 +- .../apis/security/kerberos_login.ts | 4 ++-- .../apis/security/pki_auth.ts | 4 ++-- x-pack/test_utils/kbn_server_config.ts | 4 ++-- 27 files changed, 94 insertions(+), 56 deletions(-) diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 79db96d759aa5..44610a2fd3426 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -34,7 +34,7 @@ If the {es} {security-features} are enabled, you must have the {ref}/security-privileges.html[`manage_watcher` or `monitor_watcher`] cluster privileges to use Watcher in {kib}. -Alternately, you can have the built-in `kibana_user` role +Alternately, you can have the built-in `kibana_admin` role and either of these watcher roles: * `watcher_admin`. You can perform all Watcher actions, including create and edit watches. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index df4d8a0b65ee7..146d4e97b6cf4 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -80,6 +80,21 @@ specified explicitly. *Impact:* Any workflow that involved manually clearing generated bundles will have to be updated with the new path. +[float] +[[breaking_80_user_role_changes]] +=== User role changes + +[float] +==== `kibana_user` role has been removed and `kibana_admin` has been added. + +*Details:* The `kibana_user` role has been removed and `kibana_admin` has been added to better +reflect its intended use. This role continues to grant all access to every +{kib} feature. If you wish to restrict access to specific features, create +custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. + +*Impact:* Any users currently assigned the `kibana_user` role will need to +instead be assigned the `kibana_admin` role to maintain their current +access level. [float] [[breaking_80_reporting_changes]] diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc index 2a960348b1e02..6651b33ea0e0e 100644 --- a/docs/uptime-guide/security.asciidoc +++ b/docs/uptime-guide/security.asciidoc @@ -42,7 +42,7 @@ PUT /_security/role/uptime === Assign the role to a user Next, you'll need to create a user with both the `uptime` role, and another role with sufficient {kibana-ref}/kibana-privileges.html[Kibana privileges], -such as the `kibana_user` role. +such as the `kibana_admin` role. You can do this with the following request: ["source","sh",subs="attributes,callouts"] @@ -50,7 +50,7 @@ You can do this with the following request: PUT /_security/user/jacknich { "password" : "j@rV1s", - "roles" : [ "uptime", "kibana_user" ], + "roles" : [ "uptime", "kibana_admin" ], "full_name" : "Jack Nicholson", "email" : "jacknich@example.com", "metadata" : { diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 61bcb9a49c901..11516e32400fb 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -63,7 +63,7 @@ remote monitoring cluster, you must use credentials that are valid on both the -- -.. Create users that have the `monitoring_user` and `kibana_user` +.. Create users that have the `monitoring_user` and `kibana_admin` {ref}/built-in-roles.html[built-in roles]. . Open {kib} in your web browser. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 2636b3dfc1bd3..853c735418cea 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -2,11 +2,11 @@ [[xpack-security-authorization]] === Granting access to {kib} -The Elastic Stack comes with the `kibana_user` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. +The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. -When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_user` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_user` has access to all the features in all spaces. +When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_admin` has access to all the features in all spaces. -NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_user` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. +NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. [role="xpack"] === {kib} role management diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 5f5d85fe8d3be..825580bdc772e 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -85,14 +85,14 @@ elasticsearch.username: 'custom_kibana_system' [[reporting-roles-user-api]] ==== With the user API This example uses the {ref}/security-api-put-user.html[user API] to create a user who has the -`reporting_user` role and the `kibana_user` role: +`reporting_user` role and the `kibana_admin` role: [source, sh] --------------------------------------------------------------- POST /_security/user/reporter { "password" : "x-pack-test-password", - "roles" : ["kibana_user", "reporting_user"], + "roles" : ["kibana_admin", "reporting_user"], "full_name" : "Reporting User" } --------------------------------------------------------------- @@ -106,11 +106,11 @@ roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in {ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the -`kibana_user` and `reporting_user` roles: +`kibana_admin` and `reporting_user` roles: [source,yaml] -------------------------------------------------------------------------------- -kibana_user: +kibana_admin: - "cn=Bill Murray,dc=example,dc=com" reporting_user: - "cn=Bill Murray,dc=example,dc=com" diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 2d07b57bfabe1..b6b5248777f6b 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -104,7 +104,7 @@ You can manage privileges on the *Management / Security / Roles* page in {kib}. If you're using the native realm with Basic Authentication, you can assign roles using the *Management / Security / Users* page in {kib} or the {ref}/security-api.html#security-user-apis[user management APIs]. For example, -the following creates a user named `jacknich` and assigns it the `kibana_user` +the following creates a user named `jacknich` and assigns it the `kibana_admin` role: [source,js] @@ -112,7 +112,7 @@ role: POST /_security/user/jacknich { "password" : "t0pS3cr3t", - "roles" : [ "kibana_user" ] + "roles" : [ "kibana_admin" ] } -------------------------------------------------------------------------------- // CONSOLE diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html index 8c67451b86f36..63cd4440ecf8a 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html +++ b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html @@ -15,9 +15,9 @@ class="kuiInfoPanelBody__message" i18n-id="xpack.monitoring.accessDenied.notAuthorizedDescription" i18n-default-message="You are not authorized to access Monitoring. To use Monitoring, you - need the privileges granted by both the `{kibanaUser}` and + need the privileges granted by both the `{kibanaAdmin}` and `{monitoringUser}` roles." - i18n-values="{ kibanaUser: 'kibana_user', monitoringUser: 'monitoring_user' }" + i18n-values="{ kibanaAdmin: 'kibana_admin', monitoringUser: 'monitoring_user' }" >
with user profile disabling "manageSpaces"

+ "kibanaAdmin": , diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 21cadfafe1790..b2b92356e5126 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -95,13 +95,13 @@ class SpaceAwarePrivilegeSectionUI extends Component { ), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 817aa03db31bd..47e11817ffa5d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8908,7 +8908,7 @@ "xpack.ml.validateJob.validateJobButtonLabel": "ジョブを検証", "xpack.monitoring.accessDenied.backToKibanaButtonLabel": "Kibana に戻る", "xpack.monitoring.accessDenied.clusterNotConfiguredDescription": "専用の監視クラスターへのアクセスを試みている場合、監視クラスターで構成されていないユーザーとしてログインしていることが原因である可能性があります。", - "xpack.monitoring.accessDenied.notAuthorizedDescription": "監視アクセスが許可されていません。監視を利用するには、「{kibanaUser}」と「{monitoringUser}」の両方のロールからの権限が必要です。", + "xpack.monitoring.accessDenied.notAuthorizedDescription": "監視アクセスが許可されていません。監視を利用するには、「{kibanaAdmin}」と「{monitoringUser}」の両方のロールからの権限が必要です。", "xpack.monitoring.accessDeniedTitle": "アクセス拒否", "xpack.monitoring.ajaxErrorHandler.httpErrorMessage": "HTTP {errStatus}", "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "監視リクエストエラー", @@ -10644,11 +10644,11 @@ "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource": "スペースベース権限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource": "スペース機能権限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource": "**不明**", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaUser} ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaAdmin} ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* グローバル (すべてのスペース)", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "権限が不十分です", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "選択されたスペースの全機能への完全アクセスを許可します。", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "すべて", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "すべて", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d8012bbb526c9..86d9a69dc0900 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8907,7 +8907,7 @@ "xpack.ml.validateJob.validateJobButtonLabel": "验证作业", "xpack.monitoring.accessDenied.backToKibanaButtonLabel": "返回 Kibana", "xpack.monitoring.accessDenied.clusterNotConfiguredDescription": "如果您尝试访问专用监测集群,则这可能是因为该监测集群上未配置您登录时所用的用户帐户。", - "xpack.monitoring.accessDenied.notAuthorizedDescription": "您无权访问 Monitoring。要使用 Monitoring,您同时需要 `{kibanaUser}` 和 `{monitoringUser}` 角色授予的权限。", + "xpack.monitoring.accessDenied.notAuthorizedDescription": "您无权访问 Monitoring。要使用 Monitoring,您同时需要 `{kibanaAdmin}` 和 `{monitoringUser}` 角色授予的权限。", "xpack.monitoring.accessDeniedTitle": "访问被拒绝", "xpack.monitoring.ajaxErrorHandler.httpErrorMessage": "HTTP {errStatus}", "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "Monitoring 请求错误", @@ -10643,11 +10643,11 @@ "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource": "工作区基本权限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource": "全局功能权限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource": "**未知**", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaAdmin} 角色授予的所有权限,然后重试。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* 全局(所有工作区)", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "授予对选定工作区所有功能的完全访问权限。", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "全部", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "全部", diff --git a/x-pack/test/api_integration/apis/console/feature_controls.ts b/x-pack/test/api_integration/apis/console/feature_controls.ts index 3f9a086779437..ce926f0d032c8 100644 --- a/x-pack/test/api_integration/apis/console/feature_controls.ts +++ b/x-pack/test/api_integration/apis/console/feature_controls.ts @@ -43,6 +43,29 @@ export default function securityTests({ getService }: FtrProviderContext) { } }); + it('can be accessed by kibana_admin role', async () => { + const username = 'kibana_admin'; + const roleName = 'kibana_admin'; + try { + const password = `${username}-password`; + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana admin', + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.user.delete(username); + } + }); + it('can be accessed by global all role', async () => { const username = 'global_all'; const roleName = 'global_all'; diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js index 7e6a2dbe31965..4da08d7cb9726 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js @@ -44,7 +44,7 @@ export default function({ getService }) { await security.user.create(username, { password: password, full_name: 'Limited User', - roles: ['kibana_user', 'monitoring_user'], + roles: ['kibana_admin', 'monitoring_user'], }); const { body } = await supertestWithoutAuth diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index db5e11ef367ad..35a6f2c2b382a 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -12,8 +12,8 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const security = getService('security'); describe('feature controls', () => { - const kibanaUsername = 'kibana_user'; - const kibanaUserRoleName = 'kibana_user'; + const kibanaUsername = 'kibana_admin'; + const kibanaUserRoleName = 'kibana_admin'; const kibanaUserPassword = `${kibanaUsername}-password`; diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 1189fe909ca32..b521c47585d58 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -92,7 +92,7 @@ export default function({ getService, getPageObjects }) { await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); await testSubjects.setValue('userFormEmailInput', 'example@example.com'); await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('kibana_user'); + await PageObjects.security.assignRoleToUser('kibana_admin'); await PageObjects.security.assignRoleToUser('logstash-data'); await PageObjects.security.clickSaveEditUser(); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 5761369f9e468..480fa6599e036 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -58,11 +58,11 @@ export default function({ getService, getPageObjects }) { fullname: 'dls EAST', email: 'dlstest@elastic.com', save: true, - roles: ['kibana_user', 'myroleEast'], + roles: ['kibana_admin', 'myroleEast'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.userEast.roles).to.eql(['kibana_user', 'myroleEast']); + expect(users.userEast.roles).to.eql(['kibana_admin', 'myroleEast']); expect(users.userEast.reserved).to.be(false); }); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 16e9d755bf261..93a6f9cd9e0c3 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -79,11 +79,11 @@ export default function({ getService, getPageObjects }) { fullname: 'customer one', email: 'flstest@elastic.com', save: true, - roles: ['kibana_user', 'a_viewssnrole'], + roles: ['kibana_admin', 'a_viewssnrole'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.customer1.roles).to.eql(['kibana_user', 'a_viewssnrole']); + expect(users.customer1.roles).to.eql(['kibana_admin', 'a_viewssnrole']); }); it('should add new user customer2 ', async function() { @@ -95,11 +95,11 @@ export default function({ getService, getPageObjects }) { fullname: 'customer two', email: 'flstest@elastic.com', save: true, - roles: ['kibana_user', 'a_view_no_ssn_role'], + roles: ['kibana_admin', 'a_view_no_ssn_role'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.customer2.roles).to.eql(['kibana_user', 'a_view_no_ssn_role']); + expect(users.customer2.roles).to.eql(['kibana_admin', 'a_view_no_ssn_role']); }); it('user customer1 should see ssn', async function() { diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index ece289b4a666e..4e155872d1041 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -61,13 +61,13 @@ export default function({ getService, getPageObjects }) { fullname: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', save: true, - roles: ['logstash_reader', 'kibana_user'], + roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); log.debug('roles: ', users.Rashmi.roles); - expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_user']); + expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_admin']); expect(users.Rashmi.fullname).to.eql('RashmiFirst RashmiLast'); expect(users.Rashmi.reserved).to.be(false); await PageObjects.security.forceLogout(); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index 492eddcfb9f74..a007c40a06b62 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -27,11 +27,11 @@ export default function({ getService, getPageObjects }) { fullname: 'newuserFirst newuserLast', email: 'newuser@myEmail.com', save: true, - roles: ['kibana_user', 'superuser'], + roles: ['kibana_admin', 'superuser'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.newuser.roles).to.eql(['kibana_user', 'superuser']); + expect(users.newuser.roles).to.eql(['kibana_admin', 'superuser']); expect(users.newuser.fullname).to.eql('newuserFirst newuserLast'); expect(users.newuser.email).to.eql('newuser@myEmail.com'); expect(users.newuser.reserved).to.be(false); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 3eed74881e957..9dc42553f0fdf 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -42,11 +42,11 @@ export default function({ getService, getPageObjects }) { fullname: 'LeeFirst LeeLast', email: 'lee@myEmail.com', save: true, - roles: ['kibana_user'], + roles: ['kibana_admin'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.Lee.roles).to.eql(['kibana_user']); + expect(users.Lee.roles).to.eql(['kibana_admin']); expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); expect(users.Lee.email).to.eql('lee@myEmail.com'); expect(users.Lee.reserved).to.be(false); @@ -85,7 +85,7 @@ export default function({ getService, getPageObjects }) { expect(roles.apm_user.reserved).to.be(true); expect(roles.beats_admin.reserved).to.be(true); expect(roles.beats_system.reserved).to.be(true); - expect(roles.kibana_user.reserved).to.be(true); + expect(roles.kibana_admin.reserved).to.be(true); expect(roles.kibana_system.reserved).to.be(true); expect(roles.logstash_system.reserved).to.be(true); expect(roles.monitoring_user.reserved).to.be(true); diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 6920010d67187..8de5b5e69d34d 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -14,7 +14,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { // always create this because our tear down tries to delete it await security.user.create('basic_monitoring_user', { password: 'monitoring_user_password', - roles: ['monitoring_user', 'kibana_user'], + roles: ['monitoring_user', 'kibana_admin'], full_name: 'basic monitoring', }); diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 0346da334d2f2..203f90c55aa82 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -38,7 +38,7 @@ export default function({ getService }: FtrProviderContext) { await getService('esSupertest') .post('/_security/role_mapping/krb5') .send({ - roles: ['kibana_user'], + roles: ['kibana_admin'], enabled: true, rules: { field: { 'realm.name': 'kerb1' } }, }) @@ -119,7 +119,7 @@ export default function({ getService }: FtrProviderContext) { .set('Cookie', sessionCookie.cookieString()) .expect(200, { username: 'tester@TEST.ELASTIC.CO', - roles: ['kibana_user'], + roles: ['kibana_admin'], full_name: null, email: null, metadata: { diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 4eee900e68bec..186ed824b3b6c 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -48,7 +48,7 @@ export default function({ getService }: FtrProviderContext) { .post('/_security/role_mapping/first_client_pki') .ca(CA_CERT) .send({ - roles: ['kibana_user'], + roles: ['kibana_admin'], enabled: true, rules: { field: { dn: 'CN=first_client' } }, }) @@ -107,7 +107,7 @@ export default function({ getService }: FtrProviderContext) { expect(response.body).to.eql({ username: 'first_client', - roles: ['kibana_user'], + roles: ['kibana_admin'], full_name: null, email: null, enabled: true, diff --git a/x-pack/test_utils/kbn_server_config.ts b/x-pack/test_utils/kbn_server_config.ts index 75f5ac736b7c0..3cac6ed5df014 100644 --- a/x-pack/test_utils/kbn_server_config.ts +++ b/x-pack/test_utils/kbn_server_config.ts @@ -26,9 +26,9 @@ export const TestKbnServerConfig = { }, users: [ { - username: 'kibana_user', + username: 'kibana_admin', password: 'x-pack-test-password', - roles: ['kibana_user'], + roles: ['kibana_admin'], }, ], }; From c2a6e7571b4803558b004f9d64c409a0a58ccab3 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 31 Jan 2020 12:50:17 -0700 Subject: [PATCH 7/8] [Reporting] Use ES plugin from NP (#56209) * [Reporting] Use ES plugin from NP * fix elasticsearchErrors reference * fix mocha test * convert to jest * fix the code and tests * cosmetics * fix mocha tests * fix imports * fix mocha tests * fix jest * simplify Co-authored-by: Elastic Machine --- .../csv/server/execute_job.test.js | 272 +++++++++--------- .../export_types/csv/server/execute_job.ts | 15 +- .../server/create_job/create_job.ts | 15 +- .../server/execute_job.ts | 8 +- .../server/lib/generate_csv.ts | 3 + .../server/lib/generate_csv_search.ts | 11 +- .../png/server/execute_job/index.test.js | 27 +- .../png/server/execute_job/index.ts | 2 + .../server/execute_job/index.test.js | 26 +- .../printable_pdf/server/execute_job/index.ts | 2 + x-pack/legacy/plugins/reporting/index.ts | 29 +- .../reporting/server/lib/create_queue.ts | 6 +- .../server/lib/create_worker.test.ts | 31 +- .../reporting/server/lib/create_worker.ts | 6 +- .../reporting/server/lib/enqueue_job.ts | 4 +- .../fixtures/legacy_elasticsearch.js | 34 ++- .../esqueue/__tests__/helpers/create_index.js | 6 +- .../server/lib/esqueue/__tests__/index.js | 2 +- .../server/lib/esqueue/__tests__/job.js | 16 +- .../server/lib/esqueue/__tests__/worker.js | 66 ++--- .../lib/esqueue/helpers/create_index.js | 4 +- .../reporting/server/lib/esqueue/index.js | 2 +- .../reporting/server/lib/esqueue/job.js | 6 +- .../reporting/server/lib/esqueue/worker.js | 8 +- .../reporting/server/lib/jobs_query.ts | 14 +- .../reporting/server/lib/validate/index.ts | 6 +- ...js => validate_max_content_length.test.js} | 57 ++-- .../validate/validate_max_content_length.ts | 12 +- .../legacy/plugins/reporting/server/plugin.ts | 32 +-- .../generate_from_savedobject_immediate.ts | 5 +- .../reporting/server/routes/generation.ts | 17 +- .../reporting/server/routes/jobs.test.js | 50 ++-- .../plugins/reporting/server/routes/jobs.ts | 5 +- .../server/routes/lib/job_response_handler.ts | 8 +- x-pack/legacy/plugins/reporting/types.d.ts | 13 +- 35 files changed, 441 insertions(+), 379 deletions(-) rename x-pack/legacy/plugins/reporting/server/lib/validate/{__tests__/validate_max_content_length.js => validate_max_content_length.test.js} (65%) diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index 1abc923d340e6..06ba36f600b9b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -37,9 +37,15 @@ describe('CSV Execute Job', function() { let cancellationToken; let mockServer; let clusterStub; - let callWithRequestStub; + let callAsCurrentUserStub; let uiSettingsGetStub; + const mockElasticsearch = { + dataClient: { + asScoped: () => clusterStub, + }, + }; + beforeAll(async function() { const crypto = nodeCrypto({ encryptionKey }); encryptedHeaders = await crypto.encrypt(headers); @@ -55,11 +61,11 @@ describe('CSV Execute Job', function() { _scroll_id: 'defaultScrollId', }; clusterStub = { - callWithRequest: function() {}, + callAsCurrentUser: function() {}, }; - callWithRequestStub = sinon - .stub(clusterStub, 'callWithRequest') + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') .resolves(defaultElasticsearchResponse); const configGetStub = sinon.stub(); @@ -68,7 +74,6 @@ describe('CSV Execute Job', function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); mockServer = { - expose: function() {}, fieldFormatServiceFactory: function() { const uiConfigMock = {}; uiConfigMock['format:defaultTypeMap'] = { @@ -81,13 +86,6 @@ describe('CSV Execute Job', function() { return fieldFormatsRegistry; }, - plugins: { - elasticsearch: { - getCluster: function() { - return clusterStub; - }, - }, - }, config: function() { return { get: configGetStub, @@ -117,7 +115,7 @@ describe('CSV Execute Job', function() { describe('calls getScopedSavedObjectsClient with request', function() { it('containing decrypted headers', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -135,7 +133,7 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('server.basePath') .returns(serverBasePath); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -153,7 +151,7 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('server.basePath') .returns(serverBasePath); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobBasePath = 'foo-job/basePath/'; await executeJob( 'job789', @@ -176,7 +174,7 @@ describe('CSV Execute Job', function() { it('passed scoped SavedObjectsClient to uiSettingsServiceFactory', async function() { const returnValue = Symbol(); mockServer.savedObjects.getScopedSavedObjectsClient.returns(returnValue); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -190,15 +188,15 @@ describe('CSV Execute Job', function() { }); describe('basic Elasticsearch call behavior', function() { - it('should decrypt encrypted headers and pass to callWithRequest', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + it('should decrypt encrypted headers and pass to callAsCurrentUser', async function() { + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.called).toBe(true); - expect(callWithRequestStub.firstCall.args[0].headers).toEqual(headers); + expect(callAsCurrentUserStub.called).toBe(true); + expect(callAsCurrentUserStub.firstCall.args[0]).toEqual('search'); }); it('should pass the index and body to execute the initial search', async function() { @@ -207,7 +205,7 @@ describe('CSV Execute Job', function() { testBody: true, }; - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const job = { headers: encryptedHeaders, fields: [], @@ -219,115 +217,115 @@ describe('CSV Execute Job', function() { await executeJob('job777', job, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].index).toBe(index); - expect(searchCall.args[2].body).toBe(body); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].index).toBe(index); + expect(searchCall.args[1].body).toBe(body); }); it('should pass the scrollId from the initial search to the subsequent scroll', async function() { const scrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: scrollId, }); - callWithRequestStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - const scrollCall = callWithRequestStub.secondCall; + const scrollCall = callAsCurrentUserStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); - expect(scrollCall.args[2].scrollId).toBe(scrollId); + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scrollId).toBe(scrollId); }); it('should not execute scroll if there are no hits from the search', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.callCount).toBe(2); + expect(callAsCurrentUserStub.callCount).toBe(2); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); - const clearScrollCall = callWithRequestStub.secondCall; - expect(clearScrollCall.args[1]).toBe('clearScroll'); + const clearScrollCall = callAsCurrentUserStub.secondCall; + expect(clearScrollCall.args[0]).toBe('clearScroll'); }); it('should stop executing scroll if there are no hits', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.callCount).toBe(3); + expect(callAsCurrentUserStub.callCount).toBe(3); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); - const scrollCall = callWithRequestStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); - const clearScroll = callWithRequestStub.thirdCall; - expect(clearScroll.args[1]).toBe('clearScroll'); + const clearScroll = callAsCurrentUserStub.thirdCall; + expect(clearScroll.args[0]).toBe('clearScroll'); }); it('should call clearScroll with scrollId when there are no more hits', async function() { const lastScrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: lastScrollId, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([lastScrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); }); it('calls clearScroll when there is an error iterating the hits', async function() { const lastScrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [ { @@ -341,7 +339,7 @@ describe('CSV Execute Job', function() { _scroll_id: lastScrollId, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -352,9 +350,9 @@ describe('CSV Execute Job', function() { executeJob('job123', jobParams, cancellationToken) ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([lastScrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); }); }); @@ -364,14 +362,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -392,14 +390,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -420,14 +418,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -448,14 +446,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(false); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -474,8 +472,8 @@ describe('CSV Execute Job', function() { describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { - callWithRequestStub.rejects(new Error()); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.rejects(new Error()); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -487,14 +485,14 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll call errors out', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().rejects(new Error()); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.onSecondCall().rejects(new Error()); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -508,14 +506,14 @@ describe('CSV Execute Job', function() { describe('invalid responses', function() { it('should reject Promise if search returns hits but no _scroll_id', async function() { - callWithRequestStub.resolves({ + callAsCurrentUserStub.resolves({ hits: { hits: [{}], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -529,14 +527,14 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if search returns no hits and no _scroll_id', async function() { - callWithRequestStub.resolves({ + callAsCurrentUserStub.resolves({ hits: { hits: [], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -550,21 +548,21 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll returns hits but no _scroll_id', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [{}], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -578,21 +576,21 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll returns no hits and no _scroll_id', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -610,23 +608,25 @@ describe('CSV Execute Job', function() { const scrollId = getRandomScrollId(); beforeEach(function() { - // We have to "re-stub" the callWithRequest stub here so that we can use the fakeFunction + // We have to "re-stub" the callAsCurrentUser stub here so that we can use the fakeFunction // that delays the Promise resolution so we have a chance to call cancellationToken.cancel(). // Otherwise, we get into an endless loop, and don't have a chance to call cancel - callWithRequestStub.restore(); - callWithRequestStub = sinon.stub(clusterStub, 'callWithRequest').callsFake(async function() { - await delay(1); - return { - hits: { - hits: [{}], - }, - _scroll_id: scrollId, - }; - }); + callAsCurrentUserStub.restore(); + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') + .callsFake(async function() { + await delay(1); + return { + hits: { + hits: [{}], + }, + _scroll_id: scrollId, + }; + }); }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -634,14 +634,14 @@ describe('CSV Execute Job', function() { ); await delay(250); - const callCount = callWithRequestStub.callCount; + const callCount = callAsCurrentUserStub.callCount; cancellationToken.cancel(); await delay(250); - expect(callWithRequestStub.callCount).toBe(callCount + 1); // last call is to clear the scroll + expect(callAsCurrentUserStub.callCount).toBe(callCount + 1); // last call is to clear the scroll }); it(`shouldn't call clearScroll if it never got a scrollId`, async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -649,13 +649,13 @@ describe('CSV Execute Job', function() { ); cancellationToken.cancel(); - for (let i = 0; i < callWithRequestStub.callCount; ++i) { - expect(callWithRequestStub.getCall(i).args[1]).to.not.be('clearScroll'); + for (let i = 0; i < callAsCurrentUserStub.callCount; ++i) { + expect(callAsCurrentUserStub.getCall(i).args[1]).to.not.be('clearScroll'); } }); it('should call clearScroll if it got a scrollId', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -665,15 +665,15 @@ describe('CSV Execute Job', function() { cancellationToken.cancel(); await delay(100); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([scrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([scrollId]); }); }); describe('csv content', function() { it('should write column headers to output, even if there are no results', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -685,7 +685,7 @@ describe('CSV Execute Job', function() { it('should use custom uiSettings csv:separator for header', async function() { uiSettingsGetStub.withArgs('csv:separator').returns(';'); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -697,7 +697,7 @@ describe('CSV Execute Job', function() { it('should escape column headers if uiSettings csv:quoteValues is true', async function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -709,7 +709,7 @@ describe('CSV Execute Job', function() { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(false); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -720,8 +720,8 @@ describe('CSV Execute Job', function() { }); it('should write column headers to output, when there are results', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], }, @@ -740,8 +740,8 @@ describe('CSV Execute Job', function() { }); it('should use comma separated values of non-nested fields from _source', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, @@ -761,14 +761,14 @@ describe('CSV Execute Job', function() { }); it('should concatenate the hits from multiple responses', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [{ _source: { one: 'baz', two: 'qux' } }], }, @@ -789,8 +789,8 @@ describe('CSV Execute Job', function() { }); it('should use field formatters to format fields', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, @@ -834,7 +834,7 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(1); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -867,7 +867,7 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(9); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -900,14 +900,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(9); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -941,14 +941,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(18); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -981,14 +981,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ duration: scrollDuration }); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -998,9 +998,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].scroll).toBe(scrollDuration); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].scroll).toBe(scrollDuration); }); it('passes scroll size to initial search call', async function() { @@ -1010,14 +1010,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ size: scrollSize }); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1027,9 +1027,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].size).toBe(scrollSize); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].size).toBe(scrollSize); }); it('passes scroll duration to subsequent scroll call', async function() { @@ -1039,14 +1039,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ duration: scrollDuration }); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1056,9 +1056,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const scrollCall = callWithRequestStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); - expect(scrollCall.args[2].scroll).toBe(scrollDuration); + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scroll).toBe(scrollDuration); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index fe64fdc96d904..280bbf13fa992 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import Hapi from 'hapi'; import { i18n } from '@kbn/i18n'; -import { KibanaRequest } from '../../../../../../../src/core/server'; +import { ElasticsearchServiceSetup, KibanaRequest } from '../../../../../../../src/core/server'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../../server/lib'; import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger, ServerFacade } from '../../../types'; @@ -15,8 +16,11 @@ import { createGenerateCsv } from './lib/generate_csv'; export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade, parentLogger: Logger) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); +>> = function executeJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const config = server.config(); const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); @@ -74,8 +78,11 @@ export const executeJobFactory: ExecuteJobFactory { - return callWithRequest(fakeRequest, endpoint, clientParams, options); + return callAsCurrentUser(endpoint, clientParams, options); }; const savedObjects = server.savedObjects; const savedObjectsClient = savedObjects.getScopedSavedObjectsClient( diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts index a270e3e0329fe..ddef2aa0a6268 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -6,24 +6,25 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../../server/lib'; import { CreateJobFactory, ImmediateCreateJobFn, - ServerFacade, - RequestFacade, Logger, + RequestFacade, + ServerFacade, } from '../../../../types'; import { + JobDocPayloadPanelCsv, + JobParamsPanelCsv, SavedObject, SavedObjectServiceError, SavedSearchObjectAttributesJSON, SearchPanel, TimeRangeParams, VisObjectAttributesJSON, - JobDocPayloadPanelCsv, - JobParamsPanelCsv, } from '../../types'; import { createJobSearch } from './create_job_search'; @@ -35,7 +36,11 @@ interface VisData { export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(server: ServerFacade, parentLogger: Logger) { +>> = function createJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 03f491deaa43d..b1b7b7d818200 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../../server/lib'; import { @@ -21,7 +22,11 @@ import { createGenerateCsv } from './lib'; export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade, parentLogger: Logger) { +>> = function executeJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); @@ -85,6 +90,7 @@ export const executeJobFactory: ExecuteJobFactory { export async function generateCsvSearch( req: RequestFacade, server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, searchPanel: SearchPanel, jobParams: JobParamsDiscoverCsv @@ -152,8 +152,11 @@ export async function generateCsvSearch( sort: sortConfig, }, }; - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - const callCluster = (...params: [string, object]) => callWithRequest(req, ...params); + + const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( + KibanaRequest.from(req.getRawRequest()) + ); + const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const config = server.config(); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index 4f02ab5d4c077..bb33ef9c19a1d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -32,15 +32,6 @@ beforeEach(() => { info: { protocol: 'http', }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), }, @@ -57,6 +48,12 @@ beforeEach(() => { afterEach(() => generatePngObservableFactory.mockReset()); +const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, +}; + const getMockLogger = () => new LevelLogger(); const encryptHeaders = async headers => { @@ -70,7 +67,9 @@ test(`passes browserTimezone to generatePng`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const browserTimezone = 'UTC'; await executeJob( 'pngJobId', @@ -88,7 +87,9 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); @@ -108,7 +109,9 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pngJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 7d5c69655c362..c9f370197da66 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; import { @@ -27,6 +28,7 @@ type QueuedPngExecutorFactory = ExecuteJobFactory { info: { protocol: 'http', }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), }, @@ -58,6 +49,11 @@ beforeEach(() => { afterEach(() => generatePdfObservableFactory.mockReset()); const getMockLogger = () => new LevelLogger(); +const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, +}; const encryptHeaders = async headers => { const crypto = cryptoFactory(mockServer); @@ -70,7 +66,9 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const browserTimezone = 'UTC'; await executeJob( 'pdfJobId', @@ -91,7 +89,9 @@ test(`passes browserTimezone to generatePdf`, async () => { }); test(`returns content_type of application/pdf`, async () => { - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = generatePdfObservableFactory(); @@ -111,7 +111,9 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pdfJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index dee53697c6681..162376e31216e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { ServerFacade, @@ -28,6 +29,7 @@ type QueuedPdfExecutorFactory = ExecuteJobFactory { @@ -74,10 +69,6 @@ export const reporting = (kibana: any) => { async init(server: Legacy.Server) { const coreSetup = server.newPlatform.setup.core; - const pluginsSetup: ReportingSetupDeps = { - security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, - usageCollection: server.newPlatform.setup.plugins.usageCollection, - }; const fieldFormatServiceFactory = async (uiSettings: IUiSettingsClient) => { const [, plugins] = await coreSetup.getStartServices(); @@ -90,18 +81,22 @@ export const reporting = (kibana: any) => { config: server.config, info: server.info, route: server.route.bind(server), - plugins: { - elasticsearch: server.plugins.elasticsearch, - xpack_main: server.plugins.xpack_main, - }, + plugins: { xpack_main: server.plugins.xpack_main }, savedObjects: server.savedObjects, fieldFormatServiceFactory, uiSettingsServiceFactory: server.uiSettingsServiceFactory, }; - const initializerContext = server.newPlatform.coreContext; - const plugin: ReportingPlugin = reportingPluginFactory(initializerContext, __LEGACY, this); - await plugin.setup(coreSetup, pluginsSetup); + const plugin: ReportingPlugin = reportingPluginFactory( + server.newPlatform.coreContext, + __LEGACY, + this + ); + await plugin.setup(coreSetup, { + elasticsearch: coreSetup.elasticsearch, + security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, + usageCollection: server.newPlatform.setup.plugins.usageCollection, + }); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 05b760c0c3bd6..c4e32b3ebcd99 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceSetup } from 'kibana/server'; import { ServerFacade, ExportTypesRegistry, @@ -23,6 +24,7 @@ interface CreateQueueFactoryOpts { export function createQueueFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, { exportTypesRegistry, browserDriverFactory }: CreateQueueFactoryOpts ): Esqueue { @@ -33,7 +35,7 @@ export function createQueueFactory( interval: queueConfig.indexInterval, timeout: queueConfig.timeout, dateSeparator: '.', - client: server.plugins.elasticsearch.getCluster('admin'), + client: elasticsearch.dataClient, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; @@ -41,7 +43,7 @@ export function createQueueFactory( if (queueConfig.pollEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed - const createWorker = createWorkerFactory(server, logger, { + const createWorker = createWorkerFactory(server, elasticsearch, logger, { exportTypesRegistry, browserDriverFactory, }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts index 6a5c93db32376..f5c42e5505cd1 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts @@ -5,13 +5,14 @@ */ import * as sinon from 'sinon'; -import { ServerFacade, HeadlessChromiumDriverFactory } from '../../types'; -import { ExportTypesRegistry } from './export_types_registry'; +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { HeadlessChromiumDriverFactory, ServerFacade } from '../../types'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; // @ts-ignore import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; +import { ExportTypesRegistry } from './export_types_registry'; const configGetStub = sinon.stub(); configGetStub.withArgs('xpack.reporting.queue').returns({ @@ -48,10 +49,15 @@ describe('Create Worker', () => { test('Creates a single Esqueue worker for Reporting', async () => { const exportTypesRegistry = getMockExportTypesRegistry(); - const createWorker = createWorkerFactory(getMockServer(), getMockLogger(), { - exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, - browserDriverFactory: {} as HeadlessChromiumDriverFactory, - }); + const createWorker = createWorkerFactory( + getMockServer(), + {} as ElasticsearchServiceSetup, + getMockLogger(), + { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + } + ); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); @@ -82,10 +88,15 @@ Object { { executeJobFactory: executeJobFactoryStub }, { executeJobFactory: executeJobFactoryStub }, ]); - const createWorker = createWorkerFactory(getMockServer(), getMockLogger(), { - exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, - browserDriverFactory: {} as HeadlessChromiumDriverFactory, - }); + const createWorker = createWorkerFactory( + getMockServer(), + {} as ElasticsearchServiceSetup, + getMockLogger(), + { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + } + ); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 67869016a250b..2ca638f641291 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceSetup } from 'kibana/server'; import { PLUGIN_ID } from '../../common/constants'; import { ExportTypesRegistry, HeadlessChromiumDriverFactory } from '../../types'; import { CancellationToken } from '../../common/cancellation_token'; @@ -29,6 +30,7 @@ interface CreateWorkerFactoryOpts { export function createWorkerFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, { exportTypesRegistry, browserDriverFactory }: CreateWorkerFactoryOpts ) { @@ -50,7 +52,9 @@ export function createWorkerFactory( ExportTypeDefinition >) { // TODO: the executeJobFn should be unwrapped in the register method of the export types registry - const jobExecutor = exportType.executeJobFactory(server, logger, { browserDriverFactory }); + const jobExecutor = exportType.executeJobFactory(server, elasticsearch, logger, { + browserDriverFactory, + }); jobExecutors.set(exportType.jobType, jobExecutor); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 14c57fa35dcf4..1da8a3795aacc 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,6 +5,7 @@ */ import { get } from 'lodash'; +import { ElasticsearchServiceSetup } from 'kibana/server'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; import { @@ -35,6 +36,7 @@ interface EnqueueJobFactoryOpts { export function enqueueJobFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, parentLogger: Logger, { exportTypesRegistry, esqueue }: EnqueueJobFactoryOpts ): EnqueueJobFn { @@ -61,7 +63,7 @@ export function enqueueJobFactory( } // TODO: the createJobFn should be unwrapped in the register method of the export types registry - const createJob = exportType.createJobFactory(server, logger) as CreateJobFn; + const createJob = exportType.createJobFactory(server, elasticsearch, logger) as CreateJobFn; const payload = await createJob(jobParams, headers, request); const options = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js index 31bdf7767983d..ebda7ff955b11 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js @@ -1,10 +1,14 @@ -import { uniqueId, times, random } from 'lodash'; -import * as legacyElasticsearch from 'elasticsearch'; +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ -import { constants } from '../../constants'; +import { uniqueId, times, random } from 'lodash'; +import { errors as esErrors } from 'elasticsearch'; export function ClientMock() { - this.callWithInternalUser = (endpoint, params = {}, ...rest) => { + this.callAsInternalUser = (endpoint, params = {}, ...rest) => { if (endpoint === 'indices.create') { return Promise.resolve({ acknowledged: true }); } @@ -21,12 +25,12 @@ export function ClientMock() { _seq_no: 1, _primary_term: 1, _shards: { total: shardCount, successful: shardCount, failed: 0 }, - created: true + created: true, }); } if (endpoint === 'get') { - if (params === legacyElasticsearch.errors.NotFound) return legacyElasticsearch.errors.NotFound; + if (params === esErrors.NotFound) return esErrors.NotFound; const _source = { jobtype: 'jobtype', @@ -34,7 +38,7 @@ export function ClientMock() { payload: { id: 'sample-job-1', - now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)' + now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)', }, priority: 10, @@ -43,7 +47,7 @@ export function ClientMock() { attempts: 0, max_attempts: 3, status: 'pending', - ...(rest[0] || {}) + ...(rest[0] || {}), }; return Promise.resolve({ @@ -52,7 +56,7 @@ export function ClientMock() { _seq_no: params._seq_no || 1, _primary_term: params._primary_term || 1, found: true, - _source: _source + _source: _source, }); } @@ -68,8 +72,8 @@ export function ClientMock() { _source: { created_at: new Date().toString(), number: random(0, count, true), - ...source - } + ...source, + }, }; }); return Promise.resolve({ @@ -78,13 +82,13 @@ export function ClientMock() { _shards: { total: 5, successful: 5, - failed: 0 + failed: 0, }, hits: { total: count, max_score: null, - hits: hits - } + hits: hits, + }, }); } @@ -96,7 +100,7 @@ export function ClientMock() { _seq_no: params.if_seq_no + 1 || 2, _primary_term: params.if_primary_term + 1 || 2, _shards: { total: shardCount, successful: shardCount, failed: 0 }, - created: true + created: true, }); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js index 23e9aab5bad11..2944574534a82 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js @@ -17,7 +17,7 @@ describe('Create Index', function() { beforeEach(function() { client = new ClientMock(); - createSpy = sinon.spy(client, 'callWithInternalUser').withArgs('indices.create'); + createSpy = sinon.spy(client, 'callAsInternalUser').withArgs('indices.create'); }); it('should return true', function() { @@ -75,10 +75,10 @@ describe('Create Index', function() { beforeEach(function() { client = new ClientMock(); sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('indices.exists') .callsFake(() => Promise.resolve(true)); - createSpy = client.callWithInternalUser.withArgs('indices.create'); + createSpy = client.callAsInternalUser.withArgs('indices.create'); }); it('should return true', function() { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js index 8f1ed69de5e7f..428c0f0bc0736 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js @@ -40,7 +40,7 @@ describe('Esqueue class', function() { describe('Queue construction', function() { it('should ping the ES server', function() { - const pingSpy = sinon.spy(client, 'callWithInternalUser').withArgs('ping'); + const pingSpy = sinon.spy(client, 'callAsInternalUser').withArgs('ping'); new Esqueue('esqueue', { client }); sinon.assert.calledOnce(pingSpy); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js index 2d8410c18ddea..c7812ec151b00 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js @@ -79,7 +79,7 @@ describe('Job Class', function() { beforeEach(function() { type = 'type1'; payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should create the target index', function() { @@ -121,7 +121,7 @@ describe('Job Class', function() { }); it('should refresh the index', function() { - const refreshSpy = client.callWithInternalUser.withArgs('indices.refresh'); + const refreshSpy = client.callAsInternalUser.withArgs('indices.refresh'); const job = new Job(mockQueue, index, type, payload); return job.ready.then(() => { @@ -165,9 +165,9 @@ describe('Job Class', function() { it('should emit error on client index failure', function(done) { const errMsg = 'test document index failure'; - client.callWithInternalUser.restore(); + client.callAsInternalUser.restore(); sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('index') .callsFake(() => Promise.reject(new Error(errMsg))); const job = new Job(mockQueue, index, type, payload); @@ -215,7 +215,7 @@ describe('Job Class', function() { beforeEach(function() { type = 'type1'; payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should set attempt count to 0', function() { @@ -281,7 +281,7 @@ describe('Job Class', function() { authorization: 'Basic cXdlcnR5', }, }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should index the created_by value', function() { @@ -367,10 +367,10 @@ describe('Job Class', function() { }; const job = new Job(mockQueue, index, type, payload, optionals); - return Promise.resolve(client.callWithInternalUser('get', {}, optionals)) + return Promise.resolve(client.callAsInternalUser('get', {}, optionals)) .then(doc => { sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('get') .returns(Promise.resolve(doc)); }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index bd82a9e9f99db..ad93a1882746d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -288,7 +288,7 @@ describe('Worker class', function() { describe('error handling', function() { it('should pass search errors', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject()); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -303,7 +303,7 @@ describe('Worker class', function() { describe('missing index', function() { it('should swallow error', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject({ status: 404 })); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -317,7 +317,7 @@ describe('Worker class', function() { it('should return an empty array', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject({ status: 404 })); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -343,7 +343,7 @@ describe('Worker class', function() { beforeEach(() => { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.resolve({ hits: { hits: [] } })); anchorMoment = moment(anchor); @@ -417,10 +417,10 @@ describe('Worker class', function() { type: 'test', id: 12345, }; - return mockQueue.client.callWithInternalUser('get', params).then(jobDoc => { + return mockQueue.client.callAsInternalUser('get', params).then(jobDoc => { job = jobDoc; worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -483,9 +483,9 @@ describe('Worker class', function() { }); it('should reject the promise on conflict errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .returns(Promise.reject({ statusCode: 409 })); return worker._claimJob(job).catch(err => { @@ -494,9 +494,9 @@ describe('Worker class', function() { }); it('should reject the promise on other errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .returns(Promise.reject({ statusCode: 401 })); return worker._claimJob(job).catch(err => { @@ -532,12 +532,12 @@ describe('Worker class', function() { }); afterEach(() => { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); }); it('should emit for errors from claiming job', function(done) { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); @@ -558,7 +558,7 @@ describe('Worker class', function() { it('should reject the promise if an error claiming the job', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 409 }); return worker._claimPendingJobs(getMockJobs()).catch(err => { @@ -568,7 +568,7 @@ describe('Worker class', function() { it('should get the pending job', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .resolves({ test: 'cool' }); sinon.stub(worker, '_performJob').callsFake(identity); @@ -590,10 +590,10 @@ describe('Worker class', function() { anchorMoment = moment(anchor); clock = sinon.useFakeTimers(anchorMoment.valueOf()); - return mockQueue.client.callWithInternalUser('get').then(jobDoc => { + return mockQueue.client.callAsInternalUser('get').then(jobDoc => { job = jobDoc; worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -625,18 +625,18 @@ describe('Worker class', function() { }); it('should return true on conflict errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 409 }); return worker._failJob(job).then(res => expect(res).to.equal(true)); }); it('should return false on other document update errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); return worker._failJob(job).then(res => expect(res).to.equal(false)); @@ -672,9 +672,9 @@ describe('Worker class', function() { }); it('should emit on other document update errors', function(done) { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); @@ -703,9 +703,9 @@ describe('Worker class', function() { value: random(0, 100, true), }; - return mockQueue.client.callWithInternalUser('get', {}, { payload }).then(jobDoc => { + return mockQueue.client.callAsInternalUser('get', {}, { payload }).then(jobDoc => { job = jobDoc; - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -871,7 +871,7 @@ describe('Worker class', function() { }; sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 413 }); @@ -893,7 +893,7 @@ describe('Worker class', function() { describe('search failure', function() { it('causes _processPendingJobs to reject the Promise', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .rejects(new Error('test error')); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -996,7 +996,7 @@ describe('Worker class', function() { beforeEach(function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.resolve({ hits: { hits: [] } })); }); @@ -1086,20 +1086,12 @@ describe('Format Job Object', () => { }); }); -// FAILING: https://github.com/elastic/kibana/issues/51372 -describe.skip('Get Doc Path from ES Response', () => { +describe('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function() { const responseMock = { _index: 'foo', _id: 'booId', }; - expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); - }); - it('returns the same formatted string even if there is no _doc provided', function() { - const responseMock = { - _index: 'foo', - _id: 'booId', - }; - expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); + expect(getUpdatedDocPath(responseMock)).equal('/foo/booId'); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js index 670c2907fb832..465f27a817ba7 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js @@ -78,13 +78,13 @@ export function createIndex(client, indexName, indexSettings = {}) { }; return client - .callWithInternalUser('indices.exists', { + .callAsInternalUser('indices.exists', { index: indexName, }) .then(exists => { if (!exists) { return client - .callWithInternalUser('indices.create', { + .callAsInternalUser('indices.create', { index: indexName, body: body, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js index b42ef84168940..bd30ca9ae0f29 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js @@ -32,7 +32,7 @@ export class Esqueue extends EventEmitter { } _initTasks() { - const initTasks = [this.client.callWithInternalUser('ping')]; + const initTasks = [this.client.callAsInternalUser('ping')]; return Promise.all(initTasks).catch(err => { this._logger(['initTasks', 'error'], err); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js index a7d8f4df3fd54..826fcf360a4ca 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js @@ -78,7 +78,7 @@ export class Job extends events.EventEmitter { } this.ready = createIndex(this._client, this.index, this.indexSettings) - .then(() => this._client.callWithInternalUser('index', indexParams)) + .then(() => this._client.callAsInternalUser('index', indexParams)) .then(doc => { this.document = { id: doc._id, @@ -89,7 +89,7 @@ export class Job extends events.EventEmitter { this.debug(`Job created in index ${this.index}`); return this._client - .callWithInternalUser('indices.refresh', { + .callAsInternalUser('indices.refresh', { index: this.index, }) .then(() => { @@ -111,7 +111,7 @@ export class Job extends events.EventEmitter { get() { return this.ready .then(() => { - return this._client.callWithInternalUser('get', { + return this._client.callAsInternalUser('get', { index: this.index, id: this.id, }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index 7ad84460a0c45..4373597942278 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -160,7 +160,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -199,7 +199,7 @@ export class Worker extends events.EventEmitter { }); return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -286,7 +286,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -431,7 +431,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('search', { + .callAsInternalUser('search', { index: `${this.queue.index}-*`, body: query, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 0c16f780c34ac..3562834230ea1 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { errors as elasticsearchErrors } from 'elasticsearch'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; -import { ServerFacade, JobSource } from '../../types'; +import { JobSource, ServerFacade } from '../../types'; +const esErrors = elasticsearchErrors as Record; const defaultSize = 10; interface QueryBody { @@ -34,12 +37,9 @@ interface CountAggResult { count: number; } -export function jobsQueryFactory(server: ServerFacade) { +export function jobsQueryFactory(server: ServerFacade, elasticsearch: ElasticsearchServiceSetup) { const index = server.config().get('xpack.reporting.index'); - // @ts-ignore `errors` does not exist on type Cluster - const { callWithInternalUser, errors: esErrors } = server.plugins.elasticsearch.getCluster( - 'admin' - ); + const { callAsInternalUser } = elasticsearch.adminClient; function getUsername(user: any) { return get(user, 'username', false); @@ -61,7 +61,7 @@ export function jobsQueryFactory(server: ServerFacade) { body: Object.assign(defaultBody[queryType] || {}, body), }; - return callWithInternalUser(queryType, query).catch(err => { + return callAsInternalUser(queryType, query).catch(err => { if (err instanceof esErrors['401']) return; if (err instanceof esErrors['403']) return; if (err instanceof esErrors['404']) return; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts index 79a64bd82d022..028d8fa143487 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts @@ -5,7 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { ServerFacade, Logger } from '../../../types'; +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { Logger, ServerFacade } from '../../../types'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; import { validateBrowser } from './validate_browser'; import { validateEncryptionKey } from './validate_encryption_key'; @@ -14,6 +15,7 @@ import { validateServerHost } from './validate_server_host'; export async function runValidations( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, browserFactory: HeadlessChromiumDriverFactory ) { @@ -21,7 +23,7 @@ export async function runValidations( await Promise.all([ validateBrowser(server, browserFactory, logger), validateEncryptionKey(server, logger), - validateMaxContentLength(server, logger), + validateMaxContentLength(server, elasticsearch, logger), validateServerHost(server), ]); logger.debug( diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js similarity index 65% rename from x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js rename to x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 48a58618f34cc..942dcaf842696 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -3,14 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; + import sinon from 'sinon'; -import { validateMaxContentLength } from '../validate_max_content_length'; +import { validateMaxContentLength } from './validate_max_content_length'; const FIVE_HUNDRED_MEGABYTES = 524288000; const ONE_HUNDRED_MEGABYTES = 104857600; describe('Reporting: Validate Max Content Length', () => { + const elasticsearch = { + dataClient: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }), + }, + }; + const logger = { warning: sinon.spy(), }; @@ -24,22 +36,20 @@ describe('Reporting: Validate Max Content Length', () => { config: () => ({ get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES), }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }), - }, + }; + const elasticsearch = { + dataClient: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }), }, }; - await validateMaxContentLength(server, logger); + await validateMaxContentLength(server, elasticsearch, logger); sinon.assert.calledWithMatch( logger.warning, @@ -64,22 +74,11 @@ describe('Reporting: Validate Max Content Length', () => { config: () => ({ get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES), }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }), - }, - }, }; - expect(async () => validateMaxContentLength(server, logger.warning)).not.to.throwError(); + expect( + async () => await validateMaxContentLength(server, elasticsearch, logger.warning) + ).not.toThrow(); sinon.assert.notCalled(logger.warning); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index f91cd40bfd3c7..ce4a5b93e7431 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -3,18 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import numeral from '@elastic/numeral'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { defaults, get } from 'lodash'; import { Logger, ServerFacade } from '../../../types'; const KIBANA_MAX_SIZE_BYTES_PATH = 'xpack.reporting.csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; -export async function validateMaxContentLength(server: ServerFacade, logger: Logger) { +export async function validateMaxContentLength( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + logger: Logger +) { const config = server.config(); - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); + const { callAsInternalUser } = elasticsearch.dataClient; - const elasticClusterSettingsResponse = await callWithInternalUser('cluster.getSettings', { + const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { includeDefaults: true, }); const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index cf66ec74969ca..a2938d442f7df 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -5,20 +5,26 @@ */ import { Legacy } from 'kibana'; -import { CoreSetup, CoreStart, Plugin, LoggerFactory } from 'src/core/server'; +import { + CoreSetup, + CoreStart, + ElasticsearchServiceSetup, + LoggerFactory, + Plugin, +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { PLUGIN_ID } from '../common/constants'; +import { logConfiguration } from '../log_configuration'; import { ReportingPluginSpecOptions } from '../types.d'; -import { registerRoutes } from './routes'; -import { checkLicenseFactory, getExportTypesRegistry, runValidations, LevelLogger } from './lib'; import { createBrowserDriverFactory } from './browsers'; +import { checkLicenseFactory, getExportTypesRegistry, LevelLogger, runValidations } from './lib'; +import { registerRoutes } from './routes'; import { registerReportingUsageCollector } from './usage'; -import { logConfiguration } from '../log_configuration'; -import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; export interface ReportingInitializerContext { logger: LoggerFactory; @@ -29,22 +35,16 @@ export type ReportingSetup = object; export type ReportingStart = object; export interface ReportingSetupDeps { + elasticsearch: ElasticsearchServiceSetup; usageCollection: UsageCollectionSetup; security: SecurityPluginSetup; } export type ReportingStartDeps = object; -type LegacyPlugins = Legacy.Server['plugins']; - export interface LegacySetup { config: Legacy.Server['config']; info: Legacy.Server['info']; - plugins: { - elasticsearch: LegacyPlugins['elasticsearch']; - xpack_main: XPackMainPlugin & { - status?: any; - }; - }; + plugins: { xpack_main: XPackMainPlugin & { status?: any } }; route: Legacy.Server['route']; savedObjects: Legacy.Server['savedObjects']; uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; @@ -76,10 +76,10 @@ export function reportingPluginFactory( public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise { const exportTypesRegistry = getExportTypesRegistry(); + const { usageCollection, elasticsearch } = plugins; let isCollectorReady = false; // Register a function with server to manage the collection of usage stats - const { usageCollection } = plugins; registerReportingUsageCollector( usageCollection, __LEGACY, @@ -91,7 +91,7 @@ export function reportingPluginFactory( const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); logConfiguration(__LEGACY, logger); - runValidations(__LEGACY, logger, browserDriverFactory); + runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; mirrorPluginStatus(xpackMainPlugin, legacyPlugin); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index f3ed760bba430..fd1d85fef0f21 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -36,6 +36,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( parentLogger: Logger ) { const routeOptions = getRouteOptionsCsv(server, plugins, parentLogger); + const { elasticsearch } = plugins; /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: @@ -57,8 +58,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( * * Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here */ - const createJobFn = createJobFactory(server, logger); - const executeJobFn = executeJobFactory(server, logger, { + const createJobFn = createJobFactory(server, elasticsearch, logger); + const executeJobFn = executeJobFactory(server, elasticsearch, logger, { browserDriverFactory: {} as HeadlessChromiumDriverFactory, }); const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 3c9ef6987b2d9..02a9541484bc6 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -5,6 +5,7 @@ */ import boom from 'boom'; +import { errors as elasticsearchErrors } from 'elasticsearch'; import { Legacy } from 'kibana'; import { API_BASE_URL } from '../../common/constants'; import { @@ -21,6 +22,8 @@ import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject' import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; import { makeRequestFacade } from './lib/make_request_facade'; +const esErrors = elasticsearchErrors as Record; + export function registerJobGenerationRoutes( server: ServerFacade, plugins: ReportingSetupDeps, @@ -30,11 +33,15 @@ export function registerJobGenerationRoutes( ) { const config = server.config(); const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; - // @ts-ignore TODO - const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); - - const esqueue = createQueueFactory(server, logger, { exportTypesRegistry, browserDriverFactory }); - const enqueueJob = enqueueJobFactory(server, logger, { exportTypesRegistry, esqueue }); + const { elasticsearch } = plugins; + const esqueue = createQueueFactory(server, elasticsearch, logger, { + exportTypesRegistry, + browserDriverFactory, + }); + const enqueueJob = enqueueJobFactory(server, elasticsearch, logger, { + exportTypesRegistry, + esqueue, + }); /* * Generates enqueued job details to use in responses diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js index c9d4f9fc027be..811c81c502b81 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js @@ -43,18 +43,12 @@ beforeEach(() => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', }); - mockServer.plugins = { - elasticsearch: { - getCluster: memoize(() => ({ callWithInternalUser: jest.fn() })), - createCluster: () => ({ - callWithRequest: jest.fn(), - callWithInternalUser: jest.fn(), - }), - }, - }; }); const mockPlugins = { + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, security: null, }; @@ -67,9 +61,9 @@ const getHits = (...sources) => { }; test(`returns 404 if job not found`, async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(getHits())); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -84,9 +78,11 @@ test(`returns 404 if job not found`, async () => { }); test(`returns 401 if not valid job type`, async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -101,11 +97,13 @@ test(`returns 401 if not valid job type`, async () => { describe(`when job is incomplete`, () => { const getIncompleteResponse = async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -145,9 +143,9 @@ describe(`when job is failed`, () => { status: 'failed', output: { content: 'job failure message' }, }); - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -190,9 +188,9 @@ describe(`when job is completed`, () => { title, }, }); - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index f9b731db5a702..daabc2cf22f4e 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -38,7 +38,8 @@ export function registerJobInfoRoutes( exportTypesRegistry: ExportTypesRegistry, logger: Logger ) { - const jobsQuery = jobsQueryFactory(server); + const { elasticsearch } = plugins; + const jobsQuery = jobsQueryFactory(server, elasticsearch); const getRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger); @@ -137,7 +138,7 @@ export function registerJobInfoRoutes( }); // trigger a download of the output from a job - const jobResponseHandler = jobResponseHandlerFactory(server, exportTypesRegistry); + const jobResponseHandler = jobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 3ba7aa30eedcb..62f0d0a72b389 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -5,10 +5,11 @@ */ import Boom from 'boom'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { ResponseToolkit } from 'hapi'; -import { ServerFacade, ExportTypesRegistry } from '../../../types'; -import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; +import { ExportTypesRegistry, ServerFacade } from '../../../types'; +import { jobsQueryFactory } from '../../lib/jobs_query'; import { getDocumentPayloadFactory } from './get_document_payload'; interface JobResponseHandlerParams { @@ -21,9 +22,10 @@ interface JobResponseHandlerOpts { export function jobResponseHandlerFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, exportTypesRegistry: ExportTypesRegistry ) { - const jobsQuery = jobsQueryFactory(server); + const jobsQuery = jobsQueryFactory(server, elasticsearch); const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry); return function jobResponseHandler( diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 9ba016d8b828d..a4ff39b23747d 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseObject } from 'hapi'; import { EventEmitter } from 'events'; +import { ResponseObject } from 'hapi'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { Legacy } from 'kibana'; import { CallCluster } from '../../../../src/legacy/core_plugins/elasticsearch'; import { CancellationToken } from './common/cancellation_token'; -import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; -import { LegacySetup } from './server/plugin'; +import { LevelLogger } from './server/lib/level_logger'; +import { LegacySetup, ReportingSetupDeps } from './server/plugin'; export type ReportingPlugin = object; // For Plugin contract @@ -276,10 +277,12 @@ export interface ESQueueInstance { export type CreateJobFactory = ( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) => CreateJobFnType; export type ExecuteJobFactory = ( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger, opts: { browserDriverFactory: HeadlessChromiumDriverFactory; @@ -302,10 +305,10 @@ export interface ExportTypeDefinition< validLicenses: string[]; } -export { ExportTypesRegistry } from './server/lib/export_types_registry'; +export { CancellationToken } from './common/cancellation_token'; export { HeadlessChromiumDriver } from './server/browsers/chromium/driver'; export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; -export { CancellationToken } from './common/cancellation_token'; +export { ExportTypesRegistry } from './server/lib/export_types_registry'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger }; From 8d54fa8363039a0ded5de96bb78c44b6b6bb2403 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Fri, 31 Jan 2020 13:47:41 -0700 Subject: [PATCH 8/8] [Canvas] Migrate Shareable Routes to NP (#56053) * Migrated shareables routes to NP * Added tests --- x-pack/legacy/plugins/canvas/server/plugin.ts | 3 - .../plugins/canvas/server/routes/index.ts | 12 --- .../canvas/server/routes/shareables.ts | 72 ----------------- x-pack/plugins/canvas/server/routes/index.ts | 6 +- .../server/routes/shareables/download.test.ts | 55 +++++++++++++ .../server/routes/shareables/download.ts | 33 ++++++++ .../canvas/server/routes/shareables/index.ts | 14 ++++ .../shareables/mock_shareable_workpad.json | 0 .../shareables/rendered_workpad_schema.ts | 77 ++++++++++++++++++ .../server/routes/shareables/zip.test.ts | 78 +++++++++++++++++++ .../canvas/server/routes/shareables/zip.ts | 37 +++++++++ 11 files changed, 298 insertions(+), 89 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/index.ts delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/shareables.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/download.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/download.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json create mode 100644 x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/zip.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/zip.ts diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index ac3edbabce930..1f17e85bfd294 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -5,14 +5,11 @@ */ import { CoreSetup, PluginsSetup } from './shim'; -import { routes } from './routes'; import { functions } from '../canvas_plugin_src/functions/server'; import { loadSampleData } from './sample_data'; export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { - routes(core); - plugins.interpreter.register({ serverFunctions: functions }); core.injectUiAppVars('canvas', async () => { diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts deleted file mode 100644 index 6898a3c459e3d..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shareableWorkpads } from './shareables'; -import { CoreSetup } from '../shim'; - -export function routes(setup: CoreSetup): void { - shareableWorkpads(setup.http.route); -} diff --git a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts b/x-pack/legacy/plugins/canvas/server/routes/shareables.ts deleted file mode 100644 index e8186ceceb47f..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import archiver from 'archiver'; - -import { - API_ROUTE_SHAREABLE_RUNTIME, - API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - API_ROUTE_SHAREABLE_ZIP, -} from '../../common/lib/constants'; - -import { - SHAREABLE_RUNTIME_FILE, - SHAREABLE_RUNTIME_NAME, - SHAREABLE_RUNTIME_SRC, -} from '../../shareable_runtime/constants'; - -import { CoreSetup } from '../shim'; - -export function shareableWorkpads(route: CoreSetup['http']['route']) { - // get runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME, - - handler: { - file: { - path: SHAREABLE_RUNTIME_FILE, - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - confine: false, - }, - }, - }); - - // download runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - - handler(_request, handler) { - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - // @ts-ignore No type for inert Hapi handler - const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); - file.type('application/octet-stream'); - return file; - }, - }); - - route({ - method: 'POST', - path: API_ROUTE_SHAREABLE_ZIP, - handler(request, handler) { - const workpad = request.payload; - - const archive = archiver('zip'); - archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); - archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); - archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); - - const response = handler.response(archive); - response.header('content-type', 'application/zip'); - archive.finalize(); - - return response; - }, - }); -} diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index e9afab5680332..fce278e94bf32 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -5,9 +5,10 @@ */ import { IRouter, Logger } from 'src/core/server'; -import { initWorkpadRoutes } from './workpad'; import { initCustomElementsRoutes } from './custom_elements'; import { initESFieldsRoutes } from './es_fields'; +import { initShareablesRoutes } from './shareables'; +import { initWorkpadRoutes } from './workpad'; export interface RouteInitializerDeps { router: IRouter; @@ -15,7 +16,8 @@ export interface RouteInitializerDeps { } export function initRoutes(deps: RouteInitializerDeps) { - initWorkpadRoutes(deps); initCustomElementsRoutes(deps); initESFieldsRoutes(deps); + initShareablesRoutes(deps); + initWorkpadRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts new file mode 100644 index 0000000000000..be4765217d7aa --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('fs'); + +import fs from 'fs'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +const mockRouteContext = {} as RequestHandlerContext; +const path = `api/canvas/workpad/find`; +const mockRuntime = 'Canvas shareable runtime'; + +describe('Download Canvas shareables runtime', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDownloadShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with canvas shareables runtime`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path, + }); + + const readFileSyncMock = fs.readFileSync as jest.Mock; + readFileSyncMock.mockReturnValueOnce(mockRuntime); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(`"Canvas shareable runtime"`); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.ts b/x-pack/plugins/canvas/server/routes/shareables/download.ts new file mode 100644 index 0000000000000..08bec1e4881ae --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { readFileSync } from 'fs'; +import { SHAREABLE_RUNTIME_FILE } from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeDownloadShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, + validate: false, + }, + async (_context, _request, response) => { + // TODO: check if this is still an issue on cloud after migrating to NP + // + // The option setting is not for typical use. We're using it here to avoid + // problems in Cloud environments. See elastic/kibana#47405. + // @ts-ignore No type for inert Hapi handler + // const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); + const file = readFileSync(SHAREABLE_RUNTIME_FILE); + return response.ok({ + headers: { 'content-type': 'application/octet-stream' }, + body: file, + }); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/index.ts b/x-pack/plugins/canvas/server/routes/shareables/index.ts new file mode 100644 index 0000000000000..0aabd8b955b21 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +export function initShareablesRoutes(deps: RouteInitializerDeps) { + initializeDownloadShareableWorkpadRoute(deps); + initializeZipShareableWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json b/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts new file mode 100644 index 0000000000000..792200354724e --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const ContainerStyleSchema = schema.object({ + type: schema.maybe(schema.string()), + border: schema.maybe(schema.string()), + borderRadius: schema.maybe(schema.string()), + padding: schema.maybe(schema.string()), + backgroundColor: schema.maybe(schema.string()), + backgroundImage: schema.maybe(schema.string()), + backgroundSize: schema.maybe(schema.string()), + backgroundRepeat: schema.maybe(schema.string()), + opacity: schema.maybe(schema.number()), + overflow: schema.maybe(schema.string()), +}); + +export const RenderableSchema = schema.object({ + error: schema.nullable(schema.string()), + state: schema.string(), + value: schema.object({ + as: schema.string(), + containerStyle: ContainerStyleSchema, + css: schema.maybe(schema.string()), + type: schema.string(), + value: schema.any(), + }), +}); + +export const RenderedWorkpadElementSchema = schema.object({ + expressionRenderable: RenderableSchema, + id: schema.string(), + position: PositionSchema, +}); + +export const RenderedWorkpadPageSchema = schema.object({ + id: schema.string(), + elements: schema.arrayOf(RenderedWorkpadElementSchema), + groups: schema.maybe(schema.arrayOf(schema.arrayOf(RenderedWorkpadElementSchema))), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const RenderedWorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), RenderedWorkpadPageSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(RenderedWorkpadPageSchema), + width: schema.number(), +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts new file mode 100644 index 0000000000000..edb59694a7400 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('archiver'); + +const archiver = require('archiver') as jest.Mock; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_SRC, + SHAREABLE_RUNTIME_NAME, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; + +const mockRouteContext = {} as RequestHandlerContext; +const mockWorkpad = {}; +const routePath = API_ROUTE_SHAREABLE_ZIP; + +describe('Zips Canvas shareables runtime together with workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeZipShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with zip file with runtime and workpad`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: routePath, + body: mockWorkpad, + }); + + const mockArchive = { + append: jest.fn(), + file: jest.fn(), + finalize: jest.fn(), + }; + + archiver.mockReturnValueOnce(mockArchive); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toBe(mockArchive); + expect(mockArchive.append).toHaveBeenCalledWith(JSON.stringify(mockWorkpad), { + name: 'workpad.json', + }); + expect(mockArchive.file).toHaveBeenCalledTimes(2); + expect(mockArchive.file).nthCalledWith(1, `${SHAREABLE_RUNTIME_SRC}/template.html`, { + name: 'index.html', + }); + expect(mockArchive.file).nthCalledWith(2, SHAREABLE_RUNTIME_FILE, { + name: `${SHAREABLE_RUNTIME_NAME}.js`, + }); + expect(mockArchive.finalize).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.ts new file mode 100644 index 0000000000000..e25b96cce96ff --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import archiver from 'archiver'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_NAME, + SHAREABLE_RUNTIME_SRC, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RenderedWorkpadSchema } from './rendered_workpad_schema'; +import { RouteInitializerDeps } from '..'; + +export function initializeZipShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: API_ROUTE_SHAREABLE_ZIP, + validate: { body: RenderedWorkpadSchema }, + }, + async (_context, request, response) => { + const workpad = request.body; + const archive = archiver('zip'); + archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); + archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); + archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); + + const result = { headers: { 'content-type': 'application/zip' }, body: archive }; + archive.finalize(); + + return response.ok(result); + } + ); +}