From 03d8aa15abdbe5914f8b5604cdf27269b95977ef Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 3 Feb 2020 15:02:55 -0500 Subject: [PATCH 1/8] migrate to NP router --- .../legacy/plugins/remote_clusters/index.ts | 32 ++- .../plugins/remote_clusters/kibana.json | 9 + .../legacy/plugins/remote_clusters/plugin.ts | 30 --- .../remote_cluster_edit.js | 2 +- .../plugins/remote_clusters/server/index.ts | 8 + .../call_with_request_factory.ts | 26 +++ .../lib/call_with_request_factory/index.ts | 7 + .../server/lib/is_es_error/index.ts | 7 + .../server/lib/is_es_error/is_es_error.ts | 13 ++ .../lib/license_pre_routing_factory/index.ts | 7 + .../license_pre_routing_factory.test.js | 62 ++++++ .../license_pre_routing_factory.ts | 43 ++++ .../plugins/remote_clusters/server/plugin.ts | 53 +++++ .../server/routes/api/add_route.ts | 114 +++++++---- .../server/routes/api/delete_route.ts | 192 ++++++++++-------- .../server/routes/api/get_route.ts | 77 ++++--- .../server/routes/api/update_route.ts | 130 ++++++++---- .../plugins/remote_clusters/server/types.ts | 22 ++ x-pack/legacy/plugins/remote_clusters/shim.ts | 41 ---- 19 files changed, 600 insertions(+), 275 deletions(-) create mode 100644 x-pack/legacy/plugins/remote_clusters/kibana.json delete mode 100644 x-pack/legacy/plugins/remote_clusters/plugin.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/index.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/index.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js create mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/plugin.ts create mode 100644 x-pack/legacy/plugins/remote_clusters/server/types.ts delete mode 100644 x-pack/legacy/plugins/remote_clusters/shim.ts diff --git a/x-pack/legacy/plugins/remote_clusters/index.ts b/x-pack/legacy/plugins/remote_clusters/index.ts index ed992e3bf19217..a758b8452e711c 100644 --- a/x-pack/legacy/plugins/remote_clusters/index.ts +++ b/x-pack/legacy/plugins/remote_clusters/index.ts @@ -7,8 +7,7 @@ import { Legacy } from 'kibana'; import { resolve } from 'path'; import { PLUGIN } from './common'; -import { Plugin as RemoteClustersPlugin } from './plugin'; -import { createShim } from './shim'; +import { plugin } from './server'; export function remoteClusters(kibana: any) { return new kibana.Plugin({ @@ -43,25 +42,20 @@ export function remoteClusters(kibana: any) { config.get('xpack.remote_clusters.enabled') && config.get('xpack.index_management.enabled') ); }, - init(server: Legacy.Server) { - const { - coreSetup, - pluginsSetup: { - license: { registerLicenseChecker }, - }, - } = createShim(server, PLUGIN.ID); - - const remoteClustersPlugin = new RemoteClustersPlugin(); + init(server: any) { + const { core: coreSetup } = server.newPlatform.setup; - // Set up plugin. - remoteClustersPlugin.setup(coreSetup); + const remoteClustersPluginInstance = plugin(); - registerLicenseChecker( - server, - PLUGIN.ID, - PLUGIN.getI18nName(), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); + remoteClustersPluginInstance.setup(coreSetup, { + __LEGACY: { + route: server.route.bind(server), + plugins: { + xpack_main: server.plugins.xpack_main, + remote_clusters: server.plugins[PLUGIN.ID], + }, + }, + }); }, }); } diff --git a/x-pack/legacy/plugins/remote_clusters/kibana.json b/x-pack/legacy/plugins/remote_clusters/kibana.json new file mode 100644 index 00000000000000..0934c94cfc3fee --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "remote_clusters", + "version": "kibana", + "requiredPlugins": [ + "home" + ], + "server": true, + "ui": true +} diff --git a/x-pack/legacy/plugins/remote_clusters/plugin.ts b/x-pack/legacy/plugins/remote_clusters/plugin.ts deleted file mode 100644 index a15ad553c91880..00000000000000 --- a/x-pack/legacy/plugins/remote_clusters/plugin.ts +++ /dev/null @@ -1,30 +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 { API_BASE_PATH } from './common'; -import { CoreSetup } from './shim'; -import { - registerGetRoute, - registerAddRoute, - registerUpdateRoute, - registerDeleteRoute, -} from './server/routes/api'; - -export class Plugin { - public setup(core: CoreSetup): void { - const { - http: { createRouter, isEsError }, - } = core; - - const router = createRouter(API_BASE_PATH); - - // Register routes. - registerGetRoute(router); - registerAddRoute(router); - registerUpdateRoute(router); - registerDeleteRoute(router, isEsError); - } -} diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_edit/remote_cluster_edit.js index 42b9eabc8e33eb..f48d854da7255d 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_edit/remote_cluster_edit.js @@ -37,7 +37,7 @@ export class RemoteClusterEdit extends Component { stopEditingCluster: PropTypes.func, editCluster: PropTypes.func, isEditingCluster: PropTypes.bool, - getEditClusterError: PropTypes.string, + getEditClusterError: PropTypes.object, clearEditClusterErrors: PropTypes.func, openDetailPanel: PropTypes.func, }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/index.ts b/x-pack/legacy/plugins/remote_clusters/server/index.ts new file mode 100644 index 00000000000000..0c6380a279d24d --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { RemoteClustersServerPlugin } from './plugin'; + +export const plugin = () => new RemoteClustersServerPlugin(); diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts new file mode 100644 index 00000000000000..28290f3de0e357 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts @@ -0,0 +1,26 @@ +/* + * 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 { ElasticsearchServiceSetup } from 'kibana/server'; +import { once } from 'lodash'; + +const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { + return elasticsearchService.createClient('remote_clusters', {}); +}); + +export const callWithRequestFactory = ( + elasticsearchService: ElasticsearchServiceSetup, + request: any +) => { + return (...args: any[]) => { + return ( + callWithRequest(elasticsearchService) + .asScoped(request) + // @ts-ignore + .callAsCurrentUser(...args) + ); + }; +}; diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts new file mode 100644 index 00000000000000..787814d87dff94 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/index.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/index.ts new file mode 100644 index 00000000000000..a9a3c61472d8c7 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { isEsError } from './is_es_error'; diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts new file mode 100644 index 00000000000000..4137293cf39c06 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts @@ -0,0 +1,13 @@ +/* + * 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 * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts new file mode 100644 index 00000000000000..0743e443955f45 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js new file mode 100644 index 00000000000000..b6cea09e0ea3c1 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js @@ -0,0 +1,62 @@ +/* + * 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 expect from '@kbn/expect'; +import { licensePreRoutingFactory } from '.'; +import { + LICENSE_STATUS_VALID, + LICENSE_STATUS_INVALID, +} from '../../../../../common/constants/license_status'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; + +describe('licensePreRoutingFactory()', () => { + let mockServer; + let mockLicenseCheckResults; + + beforeEach(() => { + mockServer = { + plugins: { + xpack_main: { + info: { + feature: () => ({ + getLicenseCheckResults: () => mockLicenseCheckResults, + }), + }, + }, + }, + }; + }); + + describe('status is invalid', () => { + beforeEach(() => { + mockLicenseCheckResults = { + status: LICENSE_STATUS_INVALID, + }; + }); + + it('replies with 403', () => { + const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {}); + const stubRequest = {}; + const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); + expect(response.status).to.be(403); + }); + }); + + describe('status is valid', () => { + beforeEach(() => { + mockLicenseCheckResults = { + status: LICENSE_STATUS_VALID, + }; + }); + + it('replies with nothing', () => { + const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null); + const stubRequest = {}; + const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); + expect(response).to.be(null); + }); + }); +}); diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts new file mode 100644 index 00000000000000..353510d96a00d4 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -0,0 +1,43 @@ +/* + * 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 { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; +import { PLUGIN } from '../../../common'; +import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; +import { ServerShim } from '../../types'; + +export const licensePreRoutingFactory = ( + server: ServerShim, + handler: RequestHandler +): RequestHandler => { + const xpackMainPlugin = server.plugins.xpack_main; + + // License checking and enable/disable logic + return function licensePreRouting( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + const { status } = licenseCheckResults; + + if (status !== LICENSE_STATUS_VALID) { + return response.customError({ + body: { + message: licenseCheckResults.messsage, + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; +}; diff --git a/x-pack/legacy/plugins/remote_clusters/server/plugin.ts b/x-pack/legacy/plugins/remote_clusters/server/plugin.ts new file mode 100644 index 00000000000000..50a96eb30997b0 --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { CoreSetup, Plugin } from 'src/core/server'; + +import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; +import { PLUGIN } from '../common'; +import { ServerShim, RouteDependencies } from './types'; + +import { + registerGetRoute, + registerAddRoute, + registerUpdateRoute, + registerDeleteRoute, +} from './routes/api'; + +export class RemoteClustersServerPlugin implements Plugin { + async setup( + { http, elasticsearch: elasticsearchService }: CoreSetup, + { + __LEGACY: serverShim, + }: { + __LEGACY: ServerShim; + } + ) { + const elasticsearch = await elasticsearchService.adminClient; + const router = http.createRouter(); + const routeDependencies: RouteDependencies = { + elasticsearch, + elasticsearchService, + router, + }; + + registerLicenseChecker( + serverShim as any, + PLUGIN.ID, + PLUGIN.getI18nName(), + PLUGIN.MINIMUM_LICENSE_REQUIRED + ); + + // Register routes. + registerGetRoute(routeDependencies, serverShim); + registerAddRoute(routeDependencies, serverShim); + registerUpdateRoute(routeDependencies, serverShim); + registerDeleteRoute(routeDependencies, serverShim); + } + + start() {} + + stop() {} +} diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts index 36b8d4fe7c3a0f..71791385f63aba 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts @@ -5,45 +5,87 @@ */ import { get } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { RequestHandler } from 'src/core/server'; -import { - Router, - RouterRouteHandler, - wrapCustomError, -} from '../../../../../server/lib/create_router'; import { serializeCluster } from '../../../common/cluster_serialization'; import { doesClusterExist } from '../../lib/does_cluster_exist'; +import { API_BASE_PATH } from '../../../common'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { RouteDependencies, ServerShim } from '../../types'; -export const register = (router: Router): void => { - router.post('', addHandler); -}; +export const register = (deps: RouteDependencies, legacy: ServerShim): void => { + const getAddHandler: RequestHandler = async (ctx, request, response) => { + try { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { name, seeds, skipUnavailable } = request.body; + + // Check if cluster already exists. + const existingCluster = await doesClusterExist(callWithRequest, name); + if (existingCluster) { + return response.customError({ + statusCode: 409, + body: { + message: i18n.translate( + 'xpack.remoteClusters.addRemoteCluster.existingRemoteClusterErrorMessage', + { + defaultMessage: 'There is already a remote cluster with that name.', + } + ), + }, + }); + } + + const addClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); + const updateClusterResponse = await callWithRequest('cluster.putSettings', { + body: addClusterPayload, + }); + const acknowledged = get(updateClusterResponse, 'acknowledged'); + const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); + + if (acknowledged && cluster) { + return response.ok({ + body: { + acknowledged: true, + }, + }); + } -export const addHandler: RouterRouteHandler = async (req, callWithRequest): Promise => { - const { name, seeds, skipUnavailable } = req.payload as any; - - // Check if cluster already exists. - const existingCluster = await doesClusterExist(callWithRequest, name); - if (existingCluster) { - const conflictError = wrapCustomError( - new Error('There is already a remote cluster with that name.'), - 409 - ); - - throw conflictError; - } - - const addClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); - const response = await callWithRequest('cluster.putSettings', { body: addClusterPayload }); - const acknowledged = get(response, 'acknowledged'); - const cluster = get(response, `persistent.cluster.remote.${name}`); - - if (acknowledged && cluster) { - return { - acknowledged: true, - }; - } - - // If for some reason the ES response did not acknowledge, - // return an error. This shouldn't happen. - throw wrapCustomError(new Error('Unable to add cluster, no response returned from ES.'), 400); + // If for some reason the ES response did not acknowledge, + // return an error. This shouldn't happen. + return response.customError({ + statusCode: 400, + body: { + message: i18n.translate( + 'xpack.remoteClusters.addRemoteCluster.unknownRemoteClusterErrorMessage', + { + defaultMessage: 'Unable to add cluster, no response returned from ES.', + } + ), + }, + }); + } catch (error) { + if (isEsError(error)) { + return response.customError({ statusCode: error.statusCode, body: error }); + } + return response.internalError({ body: error }); + } + }; + deps.router.post( + { + path: API_BASE_PATH, + validate: { + body: schema.object({ + name: schema.string(), + seeds: schema.arrayOf(schema.string()), + skipUnavailable: schema.boolean(), + }), + }, + }, + licensePreRoutingFactory(legacy, getAddHandler) + ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts index eff7c66b265b86..d11cc79ecbfeed 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -5,98 +5,126 @@ */ import { get } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { RequestHandler } from 'src/core/server'; -import { - Router, - RouterRouteHandler, - wrapCustomError, - wrapEsError, - wrapUnknownError, -} from '../../../../../server/lib/create_router'; +import { RouteDependencies, ServerShim } from '../../types'; import { serializeCluster } from '../../../common/cluster_serialization'; +import { API_BASE_PATH } from '../../../common'; import { doesClusterExist } from '../../lib/does_cluster_exist'; - -export const register = (router: Router, isEsError: any): void => { - router.delete('/{nameOrNames}', createDeleteHandler(isEsError)); -}; - -export const createDeleteHandler: any = (isEsError: any) => { - const deleteHandler: RouterRouteHandler = async ( - req, - callWithRequest - ): Promise<{ - itemsDeleted: any[]; - errors: any[]; - }> => { - const { nameOrNames } = req.params as any; - const names = nameOrNames.split(','); - - const itemsDeleted: any[] = []; - const errors: any[] = []; - - // Validator that returns an error if the remote cluster does not exist. - const validateClusterDoesExist = async (name: string) => { - try { - const existingCluster = await doesClusterExist(callWithRequest, name); - if (!existingCluster) { - return wrapCustomError(new Error('There is no remote cluster with that name.'), 404); +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; + +export const register = (deps: RouteDependencies, legacy: ServerShim): void => { + const getDeleteHandler: RequestHandler = async (ctx, request, response) => { + try { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { nameOrNames } = request.params; + const names = nameOrNames.split(','); + + const itemsDeleted: any[] = []; + const errors: any[] = []; + + // Validator that returns an error if the remote cluster does not exist. + const validateClusterDoesExist = async (name: string) => { + try { + const existingCluster = await doesClusterExist(callWithRequest, name); + if (!existingCluster) { + return response.customError({ + statusCode: 404, + body: { + message: i18n.translate( + 'xpack.remoteClusters.deleteRemoteCluster.noRemoteClusterErrorMessage', + { + defaultMessage: 'There is no remote cluster with that name.', + } + ), + }, + }); + } + } catch (error) { + return response.customError({ statusCode: 400, body: error }); } - } catch (error) { - return wrapCustomError(error, 400); - } - }; + }; + + // Send the request to delete the cluster and return an error if it could not be deleted. + const sendRequestToDeleteCluster = async (name: string) => { + try { + const body = serializeCluster({ name }); + const updateClusterResponse = await callWithRequest('cluster.putSettings', { body }); + const acknowledged = get(updateClusterResponse, 'acknowledged'); + const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); + + if (acknowledged && !cluster) { + return null; + } + + // If for some reason the ES response still returns the cluster information, + // return an error. This shouldn't happen. + return response.customError({ + statusCode: 400, + body: { + message: i18n.translate( + 'xpack.remoteClusters.deleteRemoteCluster.unknownRemoteClusterErrorMessage', + { + defaultMessage: 'Unable to delete cluster, information still returned from ES.', + } + ), + }, + }); + } catch (error) { + if (isEsError(error)) { + return response.customError({ statusCode: error.statusCode, body: error }); + } + return response.internalError({ body: error }); + } + }; - // Send the request to delete the cluster and return an error if it could not be deleted. - const sendRequestToDeleteCluster = async (name: string) => { - try { - const body = serializeCluster({ name }); - const response = await callWithRequest('cluster.putSettings', { body }); - const acknowledged = get(response, 'acknowledged'); - const cluster = get(response, `persistent.cluster.remote.${name}`); + const deleteCluster = async (clusterName: string) => { + // Validate that the cluster exists. + let error: any = await validateClusterDoesExist(clusterName); - if (acknowledged && !cluster) { - return null; + if (!error) { + // Delete the cluster. + error = await sendRequestToDeleteCluster(clusterName); } - // If for some reason the ES response still returns the cluster information, - // return an error. This shouldn't happen. - return wrapCustomError( - new Error('Unable to delete cluster, information still returned from ES.'), - 400 - ); - } catch (error) { - if (isEsError(error)) { - return wrapEsError(error); + if (error) { + errors.push({ name: clusterName, error }); + } else { + itemsDeleted.push(clusterName); } - - return wrapUnknownError(error); + }; + + // Delete all our cluster in parallel. + await Promise.all(names.map(deleteCluster)); + + return response.ok({ + body: { + itemsDeleted, + errors, + }, + }); + } catch (error) { + if (isEsError(error)) { + return response.customError({ statusCode: error.statusCode, body: error }); } - }; - - const deleteCluster = async (clusterName: string) => { - // Validate that the cluster exists. - let error: any = await validateClusterDoesExist(clusterName); - - if (!error) { - // Delete the cluster. - error = await sendRequestToDeleteCluster(clusterName); - } - - if (error) { - errors.push({ name: clusterName, error }); - } else { - itemsDeleted.push(clusterName); - } - }; - - // Delete all our cluster in parallel. - await Promise.all(names.map(deleteCluster)); - - return { - itemsDeleted, - errors, - }; + return response.internalError({ body: error }); + } }; - return deleteHandler; + deps.router.delete( + { + path: `${API_BASE_PATH}/{nameOrNames}`, + validate: { + params: schema.object({ + nameOrNames: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, getDeleteHandler) + ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts index 97bb59de85b899..a5f80d5c844a9c 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts @@ -6,35 +6,58 @@ import { get } from 'lodash'; -import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router'; +import { RequestHandler } from 'src/core/server'; import { deserializeCluster } from '../../../common/cluster_serialization'; +import { API_BASE_PATH } from '../../../common'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { RouteDependencies, ServerShim } from '../../types'; -export const register = (router: Router): void => { - router.get('', getAllHandler); -}; +export const register = (deps: RouteDependencies, legacy: ServerShim): void => { + const getAllHandler: RequestHandler = async (ctx, request, response) => { + try { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const clusterSettings = await callWithRequest('cluster.getSettings'); -// GET '/api/remote_clusters' -export const getAllHandler: RouterRouteHandler = async (req, callWithRequest): Promise => { - const clusterSettings = await callWithRequest('cluster.getSettings'); - const transientClusterNames = Object.keys(get(clusterSettings, `transient.cluster.remote`) || {}); - const persistentClusterNames = Object.keys( - get(clusterSettings, `persistent.cluster.remote`) || {} - ); + const transientClusterNames = Object.keys( + get(clusterSettings, 'transient.cluster.remote') || {} + ); + const persistentClusterNames = Object.keys( + get(clusterSettings, 'persistent.cluster.remote') || {} + ); + + const clustersByName = await callWithRequest('cluster.remoteInfo'); + const clusterNames = (clustersByName && Object.keys(clustersByName)) || []; + + const body = clusterNames.map((clusterName: string): any => { + const cluster = clustersByName[clusterName]; + const isTransient = transientClusterNames.includes(clusterName); + const isPersistent = persistentClusterNames.includes(clusterName); + // If the cluster hasn't been stored in the cluster state, then it's defined by the + // node's config file. + const isConfiguredByNode = !isTransient && !isPersistent; - const clustersByName = await callWithRequest('cluster.remoteInfo'); - const clusterNames = (clustersByName && Object.keys(clustersByName)) || []; - - return clusterNames.map((clusterName: string): any => { - const cluster = clustersByName[clusterName]; - const isTransient = transientClusterNames.includes(clusterName); - const isPersistent = persistentClusterNames.includes(clusterName); - // If the cluster hasn't been stored in the cluster state, then it's defined by the - // node's config file. - const isConfiguredByNode = !isTransient && !isPersistent; - - return { - ...deserializeCluster(clusterName, cluster), - isConfiguredByNode, - }; - }); + return { + ...deserializeCluster(clusterName, cluster), + isConfiguredByNode, + }; + }); + + return response.ok({ body }); + } catch (error) { + if (isEsError(error)) { + return response.customError({ statusCode: error.statusCode, body: error }); + } + return response.internalError({ body: error }); + } + }; + + deps.router.get( + { + path: API_BASE_PATH, + validate: false, + }, + licensePreRoutingFactory(legacy, getAllHandler) + ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts index d6eedf7924ca33..661c17fc06f12d 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts @@ -5,48 +5,100 @@ */ import { get } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { RequestHandler } from 'src/core/server'; -import { - Router, - RouterRouteHandler, - wrapCustomError, -} from '../../../../../server/lib/create_router'; +import { API_BASE_PATH } from '../../../common'; import { serializeCluster, deserializeCluster } from '../../../common/cluster_serialization'; import { doesClusterExist } from '../../lib/does_cluster_exist'; +import { RouteDependencies, ServerShim } from '../../types'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -export const register = (router: Router): void => { - router.put('/{name}', updateHandler); -}; +export const register = (deps: RouteDependencies, legacy: ServerShim): void => { + // TODO there are other settings that can be specified for a remote cluster via console/API that I think might cause issues when editing + const getUpdateHandler: RequestHandler = async (ctx, request, response) => { + try { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { name } = request.params; + const { seeds, skipUnavailable } = request.body; + + // Check if cluster does exist. + const existingCluster = await doesClusterExist(callWithRequest, name); + if (!existingCluster) { + return response.customError({ + statusCode: 404, + body: { + message: i18n.translate( + 'xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage', + { + defaultMessage: 'There is no remote cluster with that name.', + } + ), + }, + }); + } + + // TODO is this still needed? Issue is closed + // Delete existing cluster settings. + // This is a workaround for: https://github.com/elastic/elasticsearch/issues/37799 + const deleteClusterPayload = serializeCluster({ name }); + await callWithRequest('cluster.putSettings', { body: deleteClusterPayload }); + + // Update cluster as new settings + const updateClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); + const updateClusterResponse = await callWithRequest('cluster.putSettings', { + body: updateClusterPayload, + }); + + const acknowledged = get(updateClusterResponse, 'acknowledged'); + const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); + + if (acknowledged && cluster) { + const body = { + ...deserializeCluster(name, cluster), + isConfiguredByNode: false, + }; + return response.ok({ body }); + } + + // If for some reason the ES response did not acknowledge, + // return an error. This shouldn't happen. + return response.customError({ + statusCode: 400, + body: { + message: i18n.translate( + 'xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage', + { + defaultMessage: 'Unable to edit cluster, no response returned from ES.', + } + ), + }, + }); + } catch (error) { + if (isEsError(error)) { + return response.customError({ statusCode: error.statusCode, body: error }); + } + return response.internalError({ body: error }); + } + }; -export const updateHandler: RouterRouteHandler = async (req, callWithRequest): Promise => { - const { name } = req.params as any; - const { seeds, skipUnavailable } = req.payload as any; - - // Check if cluster does exist. - const existingCluster = await doesClusterExist(callWithRequest, name); - if (!existingCluster) { - throw wrapCustomError(new Error('There is no remote cluster with that name.'), 404); - } - - // Delete existing cluster settings. - // This is a workaround for: https://github.com/elastic/elasticsearch/issues/37799 - const deleteClusterPayload = serializeCluster({ name }); - await callWithRequest('cluster.putSettings', { body: deleteClusterPayload }); - - // Update cluster as new settings - const updateClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); - const response = await callWithRequest('cluster.putSettings', { body: updateClusterPayload }); - const acknowledged = get(response, 'acknowledged'); - const cluster = get(response, `persistent.cluster.remote.${name}`); - - if (acknowledged && cluster) { - return { - ...deserializeCluster(name, cluster), - isConfiguredByNode: false, - }; - } - - // If for some reason the ES response did not acknowledge, - // return an error. This shouldn't happen. - throw wrapCustomError(new Error('Unable to update cluster, no response returned from ES.'), 400); + deps.router.put( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + seeds: schema.arrayOf(schema.string()), + skipUnavailable: schema.boolean(), + }), + }, + }, + licensePreRoutingFactory(legacy, getUpdateHandler) + ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/types.ts b/x-pack/legacy/plugins/remote_clusters/server/types.ts new file mode 100644 index 00000000000000..99534efefcbefe --- /dev/null +++ b/x-pack/legacy/plugins/remote_clusters/server/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; + +export interface ServerShim { + route: any; + plugins: { + xpack_main: XPackMainPlugin; + remote_clusters: any; + }; +} + +export interface RouteDependencies { + router: IRouter; + elasticsearchService: ElasticsearchServiceSetup; + elasticsearch: IClusterClient; +} diff --git a/x-pack/legacy/plugins/remote_clusters/shim.ts b/x-pack/legacy/plugins/remote_clusters/shim.ts deleted file mode 100644 index d81f685992156e..00000000000000 --- a/x-pack/legacy/plugins/remote_clusters/shim.ts +++ /dev/null @@ -1,41 +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 { Legacy } from 'kibana'; -import { createRouter, isEsErrorFactory, Router } from '../../server/lib/create_router'; -import { registerLicenseChecker } from '../../server/lib/register_license_checker'; - -export interface CoreSetup { - http: { - createRouter(basePath: string): Router; - isEsError(error: any): boolean; - }; -} - -export interface Plugins { - license: { - registerLicenseChecker: typeof registerLicenseChecker; - }; -} - -export function createShim( - server: Legacy.Server, - pluginId: string -): { coreSetup: CoreSetup; pluginsSetup: Plugins } { - return { - coreSetup: { - http: { - createRouter: (basePath: string) => createRouter(server, pluginId, basePath), - isEsError: isEsErrorFactory(server), - }, - }, - pluginsSetup: { - license: { - registerLicenseChecker, - }, - }, - }; -} From 0021d90befbbd1f2ebdaabb6040435d4c9329cba Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 5 Feb 2020 11:46:53 -0500 Subject: [PATCH 2/8] move server code out of legacy and integrate NP license plugin --- .../legacy/plugins/remote_clusters/index.ts | 17 +-- .../app/store/actions/remove_clusters.js | 2 +- .../license_pre_routing_factory.ts | 43 ------ .../plugins/remote_clusters/server/plugin.ts | 53 ------- .../remote_clusters/common/constants.ts | 23 +++ .../common/lib/cluster_serialization.test.ts | 137 ++++++++++++++++++ .../common/lib/cluster_serialization.ts | 71 +++++++++ .../remote_clusters/common/lib}/index.ts | 3 +- .../plugins/remote_clusters/kibana.json | 4 +- .../plugins/remote_clusters/server/index.ts | 9 ++ .../call_with_request_factory.ts | 0 .../lib/call_with_request_factory/index.ts | 0 .../server/lib/does_cluster_exist.ts | 0 .../server/lib/is_es_error/index.ts | 0 .../server/lib/is_es_error/is_es_error.ts | 0 .../lib/license_pre_routing_factory/index.ts | 0 .../license_pre_routing_factory.test.ts} | 44 ++---- .../license_pre_routing_factory.ts | 35 +++++ .../plugins/remote_clusters/server/plugin.ts | 72 +++++++++ .../server/routes/api/add_route.test.ts | 0 .../server/routes/api/add_route.ts | 12 +- .../server/routes/api/delete_route.test.ts | 0 .../server/routes/api/delete_route.ts | 12 +- .../server/routes/api/get_route.test.ts | 0 .../server/routes/api/get_route.ts | 12 +- .../server/routes/api/index.ts | 0 .../server/routes/api/update_route.test.ts | 0 .../server/routes/api/update_route.ts | 12 +- .../plugins/remote_clusters/server/types.ts | 18 ++- 29 files changed, 396 insertions(+), 183 deletions(-) delete mode 100644 x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts delete mode 100644 x-pack/legacy/plugins/remote_clusters/server/plugin.ts create mode 100644 x-pack/plugins/remote_clusters/common/constants.ts create mode 100644 x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts create mode 100644 x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts rename x-pack/{legacy/plugins/remote_clusters/server => plugins/remote_clusters/common/lib}/index.ts (67%) rename x-pack/{legacy => }/plugins/remote_clusters/kibana.json (76%) create mode 100644 x-pack/plugins/remote_clusters/server/index.ts rename x-pack/{legacy => }/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/lib/does_cluster_exist.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/lib/is_es_error/index.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts (100%) rename x-pack/{legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js => plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts} (53%) create mode 100644 x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts create mode 100644 x-pack/plugins/remote_clusters/server/plugin.ts rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/add_route.test.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/add_route.ts (87%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/delete_route.test.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/delete_route.ts (90%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/get_route.test.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/get_route.ts (83%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/index.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/update_route.test.ts (100%) rename x-pack/{legacy => }/plugins/remote_clusters/server/routes/api/update_route.ts (90%) rename x-pack/{legacy => }/plugins/remote_clusters/server/types.ts (63%) diff --git a/x-pack/legacy/plugins/remote_clusters/index.ts b/x-pack/legacy/plugins/remote_clusters/index.ts index a758b8452e711c..5dd823e09eb8b8 100644 --- a/x-pack/legacy/plugins/remote_clusters/index.ts +++ b/x-pack/legacy/plugins/remote_clusters/index.ts @@ -7,7 +7,6 @@ import { Legacy } from 'kibana'; import { resolve } from 'path'; import { PLUGIN } from './common'; -import { plugin } from './server'; export function remoteClusters(kibana: any) { return new kibana.Plugin({ @@ -42,20 +41,6 @@ export function remoteClusters(kibana: any) { config.get('xpack.remote_clusters.enabled') && config.get('xpack.index_management.enabled') ); }, - init(server: any) { - const { core: coreSetup } = server.newPlatform.setup; - - const remoteClustersPluginInstance = plugin(); - - remoteClustersPluginInstance.setup(coreSetup, { - __LEGACY: { - route: server.route.bind(server), - plugins: { - xpack_main: server.plugins.xpack_main, - remote_clusters: server.plugins[PLUGIN.ID], - }, - }, - }); - }, + init(server: any) {}, }); } diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js index 47eb192714d7a6..e41c10e001be47 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js @@ -64,7 +64,7 @@ export const removeClusters = names => async (dispatch, getState) => { name, error: { output: { - payload: { message }, + payload: { msg: message }, }, }, } = errors[0]; diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index 353510d96a00d4..00000000000000 --- a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,43 +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 { - KibanaRequest, - KibanaResponseFactory, - RequestHandler, - RequestHandlerContext, -} from 'src/core/server'; -import { PLUGIN } from '../../../common'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; -import { ServerShim } from '../../types'; - -export const licensePreRoutingFactory = ( - server: ServerShim, - handler: RequestHandler -): RequestHandler => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - return function licensePreRouting( - ctx: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - const { status } = licenseCheckResults; - - if (status !== LICENSE_STATUS_VALID) { - return response.customError({ - body: { - message: licenseCheckResults.messsage, - }, - statusCode: 403, - }); - } - - return handler(ctx, request, response); - }; -}; diff --git a/x-pack/legacy/plugins/remote_clusters/server/plugin.ts b/x-pack/legacy/plugins/remote_clusters/server/plugin.ts deleted file mode 100644 index 50a96eb30997b0..00000000000000 --- a/x-pack/legacy/plugins/remote_clusters/server/plugin.ts +++ /dev/null @@ -1,53 +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 { CoreSetup, Plugin } from 'src/core/server'; - -import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; -import { PLUGIN } from '../common'; -import { ServerShim, RouteDependencies } from './types'; - -import { - registerGetRoute, - registerAddRoute, - registerUpdateRoute, - registerDeleteRoute, -} from './routes/api'; - -export class RemoteClustersServerPlugin implements Plugin { - async setup( - { http, elasticsearch: elasticsearchService }: CoreSetup, - { - __LEGACY: serverShim, - }: { - __LEGACY: ServerShim; - } - ) { - const elasticsearch = await elasticsearchService.adminClient; - const router = http.createRouter(); - const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, - router, - }; - - registerLicenseChecker( - serverShim as any, - PLUGIN.ID, - PLUGIN.getI18nName(), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); - - // Register routes. - registerGetRoute(routeDependencies, serverShim); - registerAddRoute(routeDependencies, serverShim); - registerUpdateRoute(routeDependencies, serverShim); - registerDeleteRoute(routeDependencies, serverShim); - } - - start() {} - - stop() {} -} diff --git a/x-pack/plugins/remote_clusters/common/constants.ts b/x-pack/plugins/remote_clusters/common/constants.ts new file mode 100644 index 00000000000000..3521b7f662fc94 --- /dev/null +++ b/x-pack/plugins/remote_clusters/common/constants.ts @@ -0,0 +1,23 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN = { + id: 'remote_clusters', + // Remote Clusters are used in both CCS and CCR, and CCS is available for all licenses. + minimumLicenseType: basicLicense, + getI18nName: (): string => { + return i18n.translate('xpack.remoteClusters.appName', { + defaultMessage: 'Remote Clusters', + }); + }, +}; + +export const API_BASE_PATH = '/api/remote_clusters'; diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts new file mode 100644 index 00000000000000..476fbee7fb6a06 --- /dev/null +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { deserializeCluster, serializeCluster } from './cluster_serialization'; + +describe('cluster_serialization', () => { + describe('deserializeCluster()', () => { + it('should throw an error for invalid arguments', () => { + expect(() => deserializeCluster('foo', 'bar')).toThrowError(); + }); + + it('should deserialize a complete cluster object', () => { + expect( + deserializeCluster('test_cluster', { + seeds: ['localhost:9300'], + connected: true, + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + transport: { + ping_schedule: '-1', + compress: false, + }, + }) + ).toEqual({ + name: 'test_cluster', + seeds: ['localhost:9300'], + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + transportPingSchedule: '-1', + transportCompress: false, + }); + }); + + it('should deserialize a cluster object without transport information', () => { + expect( + deserializeCluster('test_cluster', { + seeds: ['localhost:9300'], + connected: true, + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }) + ).toEqual({ + name: 'test_cluster', + seeds: ['localhost:9300'], + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + }); + }); + + it('should deserialize a cluster object with arbitrary missing properties', () => { + expect( + deserializeCluster('test_cluster', { + seeds: ['localhost:9300'], + connected: true, + num_nodes_connected: 1, + initial_connect_timeout: '30s', + transport: { + compress: false, + }, + }) + ).toEqual({ + name: 'test_cluster', + seeds: ['localhost:9300'], + isConnected: true, + connectedNodesCount: 1, + initialConnectTimeout: '30s', + transportCompress: false, + }); + }); + }); + + describe('serializeCluster()', () => { + it('should throw an error for invalid arguments', () => { + expect(() => serializeCluster('foo')).toThrowError(); + }); + + it('should serialize a complete cluster object to only dynamic properties', () => { + expect( + serializeCluster({ + name: 'test_cluster', + seeds: ['localhost:9300'], + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + transportPingSchedule: '-1', + transportCompress: false, + }) + ).toEqual({ + persistent: { + cluster: { + remote: { + test_cluster: { + seeds: ['localhost:9300'], + skip_unavailable: false, + }, + }, + }, + }, + }); + }); + + it('should serialize a cluster object with missing properties', () => { + expect( + serializeCluster({ + name: 'test_cluster', + seeds: ['localhost:9300'], + }) + ).toEqual({ + persistent: { + cluster: { + remote: { + test_cluster: { + seeds: ['localhost:9300'], + skip_unavailable: null, + }, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts new file mode 100644 index 00000000000000..07ea79d42b8006 --- /dev/null +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts @@ -0,0 +1,71 @@ +/* + * 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 function deserializeCluster(name: string, esClusterObject: any): any { + if (!name || !esClusterObject || typeof esClusterObject !== 'object') { + throw new Error('Unable to deserialize cluster'); + } + + const { + seeds, + connected: isConnected, + num_nodes_connected: connectedNodesCount, + max_connections_per_cluster: maxConnectionsPerCluster, + initial_connect_timeout: initialConnectTimeout, + skip_unavailable: skipUnavailable, + transport, + } = esClusterObject; + + let deserializedClusterObject: any = { + name, + seeds, + isConnected, + connectedNodesCount, + maxConnectionsPerCluster, + initialConnectTimeout, + skipUnavailable, + }; + + if (transport) { + const { ping_schedule: transportPingSchedule, compress: transportCompress } = transport; + + deserializedClusterObject = { + ...deserializedClusterObject, + transportPingSchedule, + transportCompress, + }; + } + + // It's unnecessary to send undefined values back to the client, so we can remove them. + Object.keys(deserializedClusterObject).forEach(key => { + if (deserializedClusterObject[key] === undefined) { + delete deserializedClusterObject[key]; + } + }); + + return deserializedClusterObject; +} + +export function serializeCluster(deserializedClusterObject: any): any { + if (!deserializedClusterObject || typeof deserializedClusterObject !== 'object') { + throw new Error('Unable to serialize cluster'); + } + + const { name, seeds, skipUnavailable } = deserializedClusterObject; + + return { + persistent: { + cluster: { + remote: { + [name]: { + seeds: seeds ? seeds : null, + skip_unavailable: skipUnavailable !== undefined ? skipUnavailable : null, + }, + }, + }, + }, + }; +} diff --git a/x-pack/legacy/plugins/remote_clusters/server/index.ts b/x-pack/plugins/remote_clusters/common/lib/index.ts similarity index 67% rename from x-pack/legacy/plugins/remote_clusters/server/index.ts rename to x-pack/plugins/remote_clusters/common/lib/index.ts index 0c6380a279d24d..bc67bf21af0384 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/index.ts +++ b/x-pack/plugins/remote_clusters/common/lib/index.ts @@ -3,6 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RemoteClustersServerPlugin } from './plugin'; -export const plugin = () => new RemoteClustersServerPlugin(); +export { deserializeCluster, serializeCluster } from './cluster_serialization'; diff --git a/x-pack/legacy/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json similarity index 76% rename from x-pack/legacy/plugins/remote_clusters/kibana.json rename to x-pack/plugins/remote_clusters/kibana.json index 0934c94cfc3fee..de1e3d1e268651 100644 --- a/x-pack/legacy/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -2,8 +2,8 @@ "id": "remote_clusters", "version": "kibana", "requiredPlugins": [ - "home" + "licensing" ], "server": true, - "ui": true + "ui": false } diff --git a/x-pack/plugins/remote_clusters/server/index.ts b/x-pack/plugins/remote_clusters/server/index.ts new file mode 100644 index 00000000000000..896161d82919b9 --- /dev/null +++ b/x-pack/plugins/remote_clusters/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { RemoteClustersServerPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new RemoteClustersServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts rename to x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts b/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts rename to x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/does_cluster_exist.ts b/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/does_cluster_exist.ts rename to x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/index.ts b/x-pack/plugins/remote_clusters/server/lib/is_es_error/index.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/index.ts rename to x-pack/plugins/remote_clusters/server/lib/is_es_error/index.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts b/x-pack/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts rename to x-pack/plugins/remote_clusters/server/lib/is_es_error/is_es_error.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts rename to x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/index.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts similarity index 53% rename from x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js rename to x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts index b6cea09e0ea3c1..dcb6cc536ba7a2 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js +++ b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts @@ -5,40 +5,21 @@ */ import expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '.'; -import { - LICENSE_STATUS_VALID, - LICENSE_STATUS_INVALID, -} from '../../../../../common/constants/license_status'; -import { kibanaResponseFactory } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; +import { licensePreRoutingFactory } from '../license_pre_routing_factory'; describe('licensePreRoutingFactory()', () => { - let mockServer; - let mockLicenseCheckResults; + let mockDeps; + let licenseStatus; beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; + mockDeps = { getLicenseStatus: () => licenseStatus }; }); - describe('status is invalid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_INVALID, - }; - }); - + describe('status is not valid', () => { it('replies with 403', () => { - const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {}); + licenseStatus = { valid: false }; + const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => {}); const stubRequest = {}; const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); expect(response.status).to.be(403); @@ -46,14 +27,9 @@ describe('licensePreRoutingFactory()', () => { }); describe('status is valid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_VALID, - }; - }); - it('replies with nothing', () => { - const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null); + licenseStatus = { valid: true }; + const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => null); const stubRequest = {}; const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); expect(response).to.be(null); diff --git a/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts new file mode 100644 index 00000000000000..481de968e661c5 --- /dev/null +++ b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -0,0 +1,35 @@ +/* + * 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 { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; +import { RouteDependencies } from '../../types'; + +export const licensePreRoutingFactory = ( + { getLicenseStatus }: RouteDependencies, + handler: RequestHandler +) => { + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = getLicenseStatus(); + if (!licenseStatus.valid) { + return response.forbidden({ + body: { + message: licenseStatus.message || '', + }, + }); + } + + return handler(ctx, request, response); + }; +}; diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts new file mode 100644 index 00000000000000..dd0bb536d26959 --- /dev/null +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -0,0 +1,72 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; +import { PLUGIN } from '../common/constants'; +import { LICENSE_CHECK_STATE } from '../../licensing/common/types'; +import { Dependencies, LicenseStatus, RouteDependencies } from './types'; + +import { + registerGetRoute, + registerAddRoute, + registerUpdateRoute, + registerDeleteRoute, +} from './routes/api'; + +export class RemoteClustersServerPlugin implements Plugin { + licenseStatus: LicenseStatus; + log: Logger; + + constructor({ logger }: PluginInitializerContext) { + this.log = logger.get(); + this.licenseStatus = { valid: false }; + } + + async setup( + { http, elasticsearch: elasticsearchService }: CoreSetup, + { licensing }: Dependencies + ) { + const elasticsearch = await elasticsearchService.adminClient; + const router = http.createRouter(); + const routeDependencies: RouteDependencies = { + elasticsearch, + elasticsearchService, + router, + getLicenseStatus: () => this.licenseStatus, + }; + + // Register routes + registerGetRoute(routeDependencies); + registerAddRoute(routeDependencies); + registerUpdateRoute(routeDependencies); + registerDeleteRoute(routeDependencies); + + licensing.license$.subscribe(license => { + const { state, message } = license.check(PLUGIN.id, PLUGIN.minimumLicenseType); + const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid; + if (hasRequiredLicense) { + this.licenseStatus = { valid: true }; + } else { + this.licenseStatus = { + valid: false, + message: + message || + i18n.translate('xpack.remoteClusters.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }; + if (message) { + this.log.info(message); + } + } + }); + } + + start() {} + + stop() {} +} diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.test.ts rename to x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts similarity index 87% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts rename to x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 71791385f63aba..714653e27b0798 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -9,16 +9,16 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { serializeCluster } from '../../../common/cluster_serialization'; +import { serializeCluster } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { API_BASE_PATH } from '../../../common'; +import { API_BASE_PATH } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsError } from '../../lib/is_es_error'; -import { RouteDependencies, ServerShim } from '../../types'; +import { RouteDependencies } from '../../types'; -export const register = (deps: RouteDependencies, legacy: ServerShim): void => { - const getAddHandler: RequestHandler = async (ctx, request, response) => { +export const register = (deps: RouteDependencies): void => { + const addHandler: RequestHandler = async (ctx, request, response) => { try { const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); @@ -86,6 +86,6 @@ export const register = (deps: RouteDependencies, legacy: ServerShim): void => { }), }, }, - licensePreRoutingFactory(legacy, getAddHandler) + licensePreRoutingFactory(deps, addHandler) ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.test.ts rename to x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts similarity index 90% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts rename to x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index d11cc79ecbfeed..cfffa7621006a4 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -9,16 +9,16 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { RouteDependencies, ServerShim } from '../../types'; -import { serializeCluster } from '../../../common/cluster_serialization'; -import { API_BASE_PATH } from '../../../common'; +import { RouteDependencies } from '../../types'; +import { serializeCluster } from '../../../common/lib'; +import { API_BASE_PATH } from '../../../common/constants'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -export const register = (deps: RouteDependencies, legacy: ServerShim): void => { - const getDeleteHandler: RequestHandler = async (ctx, request, response) => { +export const register = (deps: RouteDependencies): void => { + const deleteHandler: RequestHandler = async (ctx, request, response) => { try { const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); @@ -125,6 +125,6 @@ export const register = (deps: RouteDependencies, legacy: ServerShim): void => { }), }, }, - licensePreRoutingFactory(legacy, getDeleteHandler) + licensePreRoutingFactory(deps, deleteHandler) ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.test.ts rename to x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts similarity index 83% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts rename to x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index a5f80d5c844a9c..b205b3424c04b4 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -7,15 +7,15 @@ import { get } from 'lodash'; import { RequestHandler } from 'src/core/server'; -import { deserializeCluster } from '../../../common/cluster_serialization'; -import { API_BASE_PATH } from '../../../common'; +import { deserializeCluster } from '../../../common/lib'; +import { API_BASE_PATH } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsError } from '../../lib/is_es_error'; -import { RouteDependencies, ServerShim } from '../../types'; +import { RouteDependencies } from '../../types'; -export const register = (deps: RouteDependencies, legacy: ServerShim): void => { - const getAllHandler: RequestHandler = async (ctx, request, response) => { +export const register = (deps: RouteDependencies): void => { + const allHandler: RequestHandler = async (ctx, request, response) => { try { const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); const clusterSettings = await callWithRequest('cluster.getSettings'); @@ -58,6 +58,6 @@ export const register = (deps: RouteDependencies, legacy: ServerShim): void => { path: API_BASE_PATH, validate: false, }, - licensePreRoutingFactory(legacy, getAllHandler) + licensePreRoutingFactory(deps, allHandler) ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/index.ts b/x-pack/plugins/remote_clusters/server/routes/api/index.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/index.ts rename to x-pack/plugins/remote_clusters/server/routes/api/index.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.test.ts rename to x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts diff --git a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts similarity index 90% rename from x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts rename to x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 661c17fc06f12d..2d6e7ad89cb6e9 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -9,17 +9,17 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { API_BASE_PATH } from '../../../common'; -import { serializeCluster, deserializeCluster } from '../../../common/cluster_serialization'; +import { API_BASE_PATH } from '../../../common/constants'; +import { serializeCluster, deserializeCluster } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { RouteDependencies, ServerShim } from '../../types'; +import { RouteDependencies } from '../../types'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -export const register = (deps: RouteDependencies, legacy: ServerShim): void => { +export const register = (deps: RouteDependencies): void => { // TODO there are other settings that can be specified for a remote cluster via console/API that I think might cause issues when editing - const getUpdateHandler: RequestHandler = async (ctx, request, response) => { + const updateHandler: RequestHandler = async (ctx, request, response) => { try { const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); @@ -99,6 +99,6 @@ export const register = (deps: RouteDependencies, legacy: ServerShim): void => { }), }, }, - licensePreRoutingFactory(legacy, getUpdateHandler) + licensePreRoutingFactory(deps, updateHandler) ); }; diff --git a/x-pack/legacy/plugins/remote_clusters/server/types.ts b/x-pack/plugins/remote_clusters/server/types.ts similarity index 63% rename from x-pack/legacy/plugins/remote_clusters/server/types.ts rename to x-pack/plugins/remote_clusters/server/types.ts index 99534efefcbefe..708b1daf4bbad9 100644 --- a/x-pack/legacy/plugins/remote_clusters/server/types.ts +++ b/x-pack/plugins/remote_clusters/server/types.ts @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; +import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'kibana/server'; +import { LicensingPluginSetup } from '../../licensing/server'; -export interface ServerShim { - route: any; - plugins: { - xpack_main: XPackMainPlugin; - remote_clusters: any; - }; +export interface Dependencies { + licensing: LicensingPluginSetup; } export interface RouteDependencies { router: IRouter; + getLicenseStatus: () => LicenseStatus; elasticsearchService: ElasticsearchServiceSetup; elasticsearch: IClusterClient; } + +export interface LicenseStatus { + valid: boolean; + message?: string; +} From 6fd2c7f445c25548a60d8f26332940c01aa76323 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 6 Feb 2020 12:38:48 -0500 Subject: [PATCH 3/8] update tests + remove use of callWithRequestFactory() --- .../call_with_request_factory.ts | 26 -- .../lib/call_with_request_factory/index.ts | 7 - .../server/lib/does_cluster_exist.ts | 4 +- .../server/routes/api/add_route.test.ts | 264 ++++++++----- .../server/routes/api/add_route.ts | 7 +- .../server/routes/api/delete_route.test.ts | 364 +++++++++++------- .../server/routes/api/delete_route.ts | 7 +- .../server/routes/api/get_route.test.ts | 200 ++++++++-- .../server/routes/api/get_route.ts | 7 +- .../server/routes/api/update_route.test.ts | 292 +++++++++----- .../server/routes/api/update_route.ts | 13 +- 11 files changed, 788 insertions(+), 403 deletions(-) delete mode 100644 x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts delete mode 100644 x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts diff --git a/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts deleted file mode 100644 index 28290f3de0e357..00000000000000 --- a/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/call_with_request_factory.ts +++ /dev/null @@ -1,26 +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 { ElasticsearchServiceSetup } from 'kibana/server'; -import { once } from 'lodash'; - -const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { - return elasticsearchService.createClient('remote_clusters', {}); -}); - -export const callWithRequestFactory = ( - elasticsearchService: ElasticsearchServiceSetup, - request: any -) => { - return (...args: any[]) => { - return ( - callWithRequest(elasticsearchService) - .asScoped(request) - // @ts-ignore - .callAsCurrentUser(...args) - ); - }; -}; diff --git a/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts b/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/index.ts deleted file mode 100644 index 787814d87dff94..00000000000000 --- a/x-pack/plugins/remote_clusters/server/lib/call_with_request_factory/index.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 { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts b/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts index 1e450cf4ae920f..8f3e828f790865 100644 --- a/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts +++ b/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export async function doesClusterExist(callWithRequest: any, clusterName: string): Promise { +export async function doesClusterExist(callAsCurrentUser: any, clusterName: string): Promise { try { - const clusterInfoByName = await callWithRequest('cluster.remoteInfo'); + const clusterInfoByName = await callAsCurrentUser('cluster.remoteInfo'); return Boolean(clusterInfoByName && clusterInfoByName[clusterName]); } catch (err) { throw new Error('Unable to check if cluster already exists.'); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index 0ed2f85fa904f8..a6edd15995d728 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -3,110 +3,192 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { register } from './add_route'; +import { API_BASE_PATH } from '../../../common/constants'; +import { LicenseStatus } from '../../types'; + +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, +} from '../../../../../../src/core/server/mocks'; + +interface TestOptions { + licenseCheckResult?: LicenseStatus; + apiResponses?: Array<() => Promise>; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + payload?: Record; +} + +describe('ADD remote clusters', () => { + const addRemoteClustersTest = ( + description: string, + { licenseCheckResult = { valid: true }, apiResponses = [], asserts, payload }: TestOptions + ) => { + test(description, async () => { + const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + + const mockRouteDependencies = { + router: httpServiceMock.createRouter(), + getLicenseStatus: () => licenseCheckResult, + elasticsearchService: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, + }; + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + + elasticsearchServiceMock + .createClusterClient() + .asScoped.mockReturnValue(mockScopedClusterClient); + + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + register(mockRouteDependencies); + const [[{ validate }, handler]] = mockRouteDependencies.router.post.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'post', + path: API_BASE_PATH, + body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, + headers: { authorization: 'foo' }, + }); -import Boom from 'boom'; -import { Request, ResponseToolkit } from 'hapi'; -import { wrapCustomError } from '../../../../../server/lib/create_router'; -import { addHandler } from './add_route'; - -describe('[API Routes] Remote Clusters addHandler()', () => { - const mockResponseToolkit = {} as ResponseToolkit; - - it('returns success', async () => { - const mockCreateRequest = ({ + const mockContext = ({ + core: { + elasticsearch: { + dataClient: mockScopedClusterClient, + }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + }); + }; + + describe('success', () => { + addRemoteClustersTest('adds remote cluster', { + apiResponses: [ + async () => ({}), + async () => ({ + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }, + }, + }, + transient: {}, + }), + ], payload: { - name: 'test_cluster', - seeds: [], + name: 'test', + seeds: ['127.0.0.1:9300'], + skipUnavailable: false, }, - } as unknown) as Request; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce(null) - .mockReturnValueOnce({ - acknowledged: true, - persistent: { - cluster: { - remote: { - test_cluster: { - cluster: true, + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + }, + }, }, }, - }, + ], + ], + statusCode: 200, + result: { + acknowledged: true, }, - }); - - const response = await addHandler(mockCreateRequest, callWithRequest, mockResponseToolkit); - const expectedResponse = { - acknowledged: true, - }; - expect(response).toEqual(expectedResponse); - }); - - it('throws an error if the response does not contain cluster information', async () => { - const mockCreateRequest = ({ - payload: { - name: 'test_cluster', - seeds: [], }, - } as unknown) as Request; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce(null) - .mockReturnValueOnce({ - acknowledged: true, - persistent: {}, - }); - - const expectedError = wrapCustomError( - new Error('Unable to add cluster, no response returned from ES.'), - 400 - ); - - await expect( - addHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) - ).rejects.toThrow(expectedError); + }); }); - it('throws an error if the cluster already exists', async () => { - const mockCreateRequest = ({ + describe('failure', () => { + addRemoteClustersTest('returns 409 if remote cluster already exists', { + apiResponses: [ + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + ], payload: { - name: 'test_cluster', - seeds: [], + name: 'test', + seeds: ['127.0.0.1:9300'], + skipUnavailable: false, }, - } as unknown) as Request; - - const callWithRequest = jest.fn().mockReturnValueOnce({ test_cluster: true }); - - const expectedError = wrapCustomError( - new Error('There is already a remote cluster with that name.'), - 409 - ); - - await expect( - addHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) - ).rejects.toThrow(expectedError); - }); + asserts: { + apiArguments: [['cluster.remoteInfo']], + statusCode: 409, + result: { + message: 'There is already a remote cluster with that name.', + }, + }, + }); - it('throws an ES error when one is received', async () => { - const mockCreateRequest = ({ + addRemoteClustersTest('returns 400 ES did not acknowledge remote cluster', { + apiResponses: [async () => ({}), async () => ({})], payload: { - name: 'test_cluster', - seeds: [], + name: 'test', + seeds: ['127.0.0.1:9300'], + skipUnavailable: false, }, - } as unknown) as Request; - - const mockError = new Error() as any; - mockError.response = JSON.stringify({ error: 'Test error' }); - - const callWithRequest = jest - .fn() - .mockReturnValueOnce(null) - .mockRejectedValueOnce(mockError); - - await expect( - addHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) - ).rejects.toThrow(Boom.boomify(mockError)); + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + }, + }, + }, + }, + ], + ], + statusCode: 400, + result: { + message: 'Unable to add cluster, no response returned from ES.', + }, + }, + }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 714653e27b0798..8fbc964f9a7306 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -13,19 +13,18 @@ import { serializeCluster } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { API_BASE_PATH } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsError } from '../../lib/is_es_error'; import { RouteDependencies } from '../../types'; export const register = (deps: RouteDependencies): void => { const addHandler: RequestHandler = async (ctx, request, response) => { try { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; const { name, seeds, skipUnavailable } = request.body; // Check if cluster already exists. - const existingCluster = await doesClusterExist(callWithRequest, name); + const existingCluster = await doesClusterExist(callAsCurrentUser, name); if (existingCluster) { return response.customError({ statusCode: 409, @@ -41,7 +40,7 @@ export const register = (deps: RouteDependencies): void => { } const addClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); - const updateClusterResponse = await callWithRequest('cluster.putSettings', { + const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body: addClusterPayload, }); const acknowledged = get(updateClusterResponse, 'acknowledged'); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index b7eeffcb751054..04deb62d2c2d26 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -3,156 +3,244 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { register } from './delete_route'; +import { API_BASE_PATH } from '../../../common/constants'; +import { LicenseStatus } from '../../types'; + +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, +} from '../../../../../../src/core/server/mocks'; + +interface TestOptions { + licenseCheckResult?: LicenseStatus; + apiResponses?: Array<() => Promise>; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + params: { + nameOrNames: string; + }; +} + +describe('DELETE remote clusters', () => { + const deleteRemoteClustersTest = ( + description: string, + { licenseCheckResult = { valid: true }, apiResponses = [], asserts, params }: TestOptions + ) => { + test(description, async () => { + const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + + const mockRouteDependencies = { + router: httpServiceMock.createRouter(), + getLicenseStatus: () => licenseCheckResult, + elasticsearchService: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, + }; + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + + elasticsearchServiceMock + .createClusterClient() + .asScoped.mockReturnValue(mockScopedClusterClient); + + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + register(mockRouteDependencies); + const [[{ validate }, handler]] = mockRouteDependencies.router.delete.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `${API_BASE_PATH}/{nameOrNames}`, + params: (validate as any).params.validate(params), + headers: { authorization: 'foo' }, + }); -import Boom from 'boom'; -import { Request, ResponseToolkit } from 'hapi'; -import { wrapCustomError } from '../../../../../server/lib/create_router'; -import { createDeleteHandler } from './delete_route'; - -describe('[API Routes] Remote Clusters deleteHandler()', () => { - const mockResponseToolkit = {} as ResponseToolkit; - - const isEsError = () => true; - const deleteHandler = createDeleteHandler(isEsError); - - it('returns names of deleted remote cluster', async () => { - const mockCreateRequest = ({ - params: { - nameOrNames: 'test_cluster', - }, - } as unknown) as Request; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce({ test_cluster: true }) - .mockReturnValueOnce({ - acknowledged: true, - persistent: { - cluster: { - remote: {}, + const mockContext = ({ + core: { + elasticsearch: { + dataClient: mockScopedClusterClient, }, }, - }); - - const response = await deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit); - const expectedResponse = { errors: [], itemsDeleted: ['test_cluster'] }; - expect(response).toEqual(expectedResponse); - }); - - it('returns names of multiple deleted remote clusters', async () => { - const mockCreateRequest = ({ + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + }); + }; + + describe('success', () => { + deleteRemoteClustersTest('deletes remote cluster', { + apiResponses: [ + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + async () => ({ + acknowledged: true, + persistent: {}, + transient: {}, + }), + ], params: { - nameOrNames: 'test_cluster1,test_cluster2', + nameOrNames: 'test', }, - } as unknown) as Request; - - const clusterExistsEsResponseMock = { test_cluster1: true, test_cluster2: true }; - - const successfulDeletionEsResponseMock = { - acknowledged: true, - persistent: { - cluster: { - remote: {}, + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { test: { seeds: null, skip_unavailable: null } }, + }, + }, + }, + }, + ], + ], + statusCode: 200, + result: { + itemsDeleted: ['test'], + errors: [], }, }, - }; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce(clusterExistsEsResponseMock) - .mockReturnValueOnce(clusterExistsEsResponseMock) - .mockReturnValueOnce(successfulDeletionEsResponseMock) - .mockReturnValueOnce(successfulDeletionEsResponseMock); - - const response = await deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit); - const expectedResponse = { errors: [], itemsDeleted: ['test_cluster1', 'test_cluster2'] }; - expect(response).toEqual(expectedResponse); + }); }); - it('returns an error if the response contains cluster information', async () => { - const mockCreateRequest = ({ - params: { - nameOrNames: 'test_cluster', - }, - } as unknown) as Request; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce({ test_cluster: true }) - .mockReturnValueOnce({ - acknowledged: true, - persistent: { - cluster: { - remote: { - test_cluster: {}, - }, - }, + describe('failure', () => { + deleteRemoteClustersTest( + 'returns errors array with 404 error if remote cluster does not exist', + { + apiResponses: [async () => ({})], + params: { + nameOrNames: 'test', }, - }); - - const response = await deleteHandler(mockCreateRequest, callWithRequest); - const expectedResponse = { - errors: [ - { - name: 'test_cluster', - error: wrapCustomError( - new Error('Unable to delete cluster, information still returned from ES.'), - 400 - ), + asserts: { + apiArguments: [['cluster.remoteInfo']], + statusCode: 200, + result: { + errors: [ + { + error: { + options: { + body: { + message: 'There is no remote cluster with that name.', + }, + statusCode: 404, + }, + payload: { + message: 'There is no remote cluster with that name.', + }, + status: 404, + }, + name: 'test', + }, + ], + itemsDeleted: [], + }, }, - ], - itemsDeleted: [], - }; - expect(response).toEqual(expectedResponse); - }); - - it(`returns an error if the cluster doesn't exist`, async () => { - const mockCreateRequest = ({ - params: { - nameOrNames: 'test_cluster', - }, - } as unknown) as Request; - - const callWithRequest = jest.fn().mockReturnValueOnce({}); - - const response = await deleteHandler(mockCreateRequest, callWithRequest); - const expectedResponse = { - errors: [ - { - name: 'test_cluster', - error: wrapCustomError(new Error('There is no remote cluster with that name.'), 404), + } + ); + + deleteRemoteClustersTest( + 'returns errors array with 400 error if ES still returns cluster information', + { + apiResponses: [ + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + async () => ({ + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: true, + }, + }, + }, + }, + transient: {}, + }), + ], + params: { + nameOrNames: 'test', }, - ], - itemsDeleted: [], - }; - expect(response).toEqual(expectedResponse); - }); - - it('forwards an ES error when one is received', async () => { - const mockCreateRequest = ({ - params: { - nameOrNames: 'test_cluster', - }, - } as unknown) as Request; - - const mockError = new Error() as any; - mockError.response = JSON.stringify({ error: 'Test error' }); - - const callWithRequest = jest - .fn() - .mockReturnValueOnce({ test_cluster: true }) - .mockRejectedValueOnce(mockError); - - const response = await deleteHandler(mockCreateRequest, callWithRequest); - const expectedResponse = { - errors: [ - { - name: 'test_cluster', - error: Boom.boomify(mockError), + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { test: { seeds: null, skip_unavailable: null } }, + }, + }, + }, + }, + ], + ], + statusCode: 200, + result: { + errors: [ + { + error: { + options: { + body: { + message: 'Unable to delete cluster, information still returned from ES.', + }, + statusCode: 400, + }, + payload: { + message: 'Unable to delete cluster, information still returned from ES.', + }, + status: 400, + }, + name: 'test', + }, + ], + itemsDeleted: [], + }, }, - ], - itemsDeleted: [], - }; - expect(response).toEqual(expectedResponse); + } + ); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index cfffa7621006a4..0e1f06f483aca7 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -15,12 +15,11 @@ import { API_BASE_PATH } from '../../../common/constants'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; export const register = (deps: RouteDependencies): void => { const deleteHandler: RequestHandler = async (ctx, request, response) => { try { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; const { nameOrNames } = request.params; const names = nameOrNames.split(','); @@ -31,7 +30,7 @@ export const register = (deps: RouteDependencies): void => { // Validator that returns an error if the remote cluster does not exist. const validateClusterDoesExist = async (name: string) => { try { - const existingCluster = await doesClusterExist(callWithRequest, name); + const existingCluster = await doesClusterExist(callAsCurrentUser, name); if (!existingCluster) { return response.customError({ statusCode: 404, @@ -54,7 +53,7 @@ export const register = (deps: RouteDependencies): void => { const sendRequestToDeleteCluster = async (name: string) => { try { const body = serializeCluster({ name }); - const updateClusterResponse = await callWithRequest('cluster.putSettings', { body }); + const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body }); const acknowledged = get(updateClusterResponse, 'acknowledged'); const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 4599e1b1e52e1a..90955be85859d4 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -3,38 +3,188 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; -import { Request, ResponseToolkit } from 'hapi'; -import { getAllHandler } from './get_route'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { register } from './get_route'; +import { API_BASE_PATH } from '../../../common/constants'; +import { LicenseStatus } from '../../types'; -describe('[API Routes] Remote Clusters getAllHandler()', () => { - const mockResponseToolkit = {} as ResponseToolkit; +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, +} from '../../../../../../src/core/server/mocks'; - it('converts the ES response object to an array', async () => { - const callWithRequest = jest - .fn() - .mockReturnValueOnce({}) - .mockReturnValueOnce({ - abc: { seeds: ['xyz'] }, - foo: { seeds: ['bar'] }, +interface TestOptions { + licenseCheckResult?: LicenseStatus; + apiResponses?: Array<() => Promise>; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; +} + +describe('GET remote clusters', () => { + const getRemoteClustersTest = ( + description: string, + { licenseCheckResult = { valid: true }, apiResponses = [], asserts }: TestOptions + ) => { + test(description, async () => { + const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + + const mockRouteDependencies = { + router: httpServiceMock.createRouter(), + getLicenseStatus: () => licenseCheckResult, + elasticsearchService: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, + }; + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + + elasticsearchServiceMock + .createClusterClient() + .asScoped.mockReturnValue(mockScopedClusterClient); + + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + register(mockRouteDependencies); + const [[, handler]] = mockRouteDependencies.router.get.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: API_BASE_PATH, + headers: { authorization: 'foo' }, }); - const response = await getAllHandler({} as Request, callWithRequest, mockResponseToolkit); - const expectedResponse: any[] = [ - { name: 'abc', seeds: ['xyz'], isConfiguredByNode: true }, - { name: 'foo', seeds: ['bar'], isConfiguredByNode: true }, - ]; - expect(response).toEqual(expectedResponse); + const mockContext = ({ + core: { + elasticsearch: { + dataClient: mockScopedClusterClient, + }, + }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + }); + }; + + describe('success', () => { + getRemoteClustersTest('returns remote clusters', { + apiResponses: [ + async () => ({ + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + }, + }, + }, + }, + transient: {}, + }), + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + ], + asserts: { + apiArguments: [['cluster.getSettings'], ['cluster.remoteInfo']], + statusCode: 200, + result: [ + { + name: 'test', + seeds: ['127.0.0.1:9300'], + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + isConfiguredByNode: false, + }, + ], + }, + }); + getRemoteClustersTest('returns an empty array when ES responds with an empty object', { + apiResponses: [async () => ({}), async () => ({})], + asserts: { + apiArguments: [['cluster.getSettings'], ['cluster.remoteInfo']], + statusCode: 200, + result: [], + }, + }); }); - it('returns an empty array when ES responds with an empty object', async () => { - const callWithRequest = jest - .fn() - .mockReturnValueOnce({}) - .mockReturnValueOnce({}); + describe('failure', () => { + const error = Boom.notAcceptable('test error'); + + getRemoteClustersTest('returns an error if failure to get cluster settings', { + apiResponses: [ + async () => { + throw error; + }, + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + ], + asserts: { + apiArguments: [['cluster.getSettings']], + statusCode: 500, + result: error, + }, + }); - const response = await getAllHandler({} as Request, callWithRequest, mockResponseToolkit); - const expectedResponse: any[] = []; - expect(response).toEqual(expectedResponse); + getRemoteClustersTest('returns an error if failure to get cluster remote info', { + apiResponses: [ + async () => ({ + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + }, + }, + }, + }, + transient: {}, + }), + async () => { + throw error; + }, + ], + asserts: { + apiArguments: [['cluster.getSettings'], ['cluster.remoteInfo']], + statusCode: 500, + result: error, + }, + }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index b205b3424c04b4..46b3eb74c0ff12 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -10,15 +10,14 @@ import { RequestHandler } from 'src/core/server'; import { deserializeCluster } from '../../../common/lib'; import { API_BASE_PATH } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsError } from '../../lib/is_es_error'; import { RouteDependencies } from '../../types'; export const register = (deps: RouteDependencies): void => { const allHandler: RequestHandler = async (ctx, request, response) => { try { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const clusterSettings = await callWithRequest('cluster.getSettings'); + const callAsCurrentUser = await ctx.core.elasticsearch.dataClient.callAsCurrentUser; + const clusterSettings = await callAsCurrentUser('cluster.getSettings'); const transientClusterNames = Object.keys( get(clusterSettings, 'transient.cluster.remote') || {} @@ -27,7 +26,7 @@ export const register = (deps: RouteDependencies): void => { get(clusterSettings, 'persistent.cluster.remote') || {} ); - const clustersByName = await callWithRequest('cluster.remoteInfo'); + const clustersByName = await callAsCurrentUser('cluster.remoteInfo'); const clusterNames = (clustersByName && Object.keys(clustersByName)) || []; const body = clusterNames.map((clusterName: string): any => { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 4de92aef78357e..9ba239c3ff6616 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -3,118 +3,226 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { register } from './update_route'; +import { API_BASE_PATH } from '../../../common/constants'; +import { LicenseStatus } from '../../types'; -import { Request, ResponseToolkit } from 'hapi'; -import { wrapCustomError } from '../../../../../server/lib/create_router'; -import { updateHandler } from './update_route'; +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, +} from '../../../../../../src/core/server/mocks'; -describe('[API Routes] Remote Clusters updateHandler()', () => { - const mockResponseToolkit = {} as ResponseToolkit; +interface TestOptions { + licenseCheckResult?: LicenseStatus; + apiResponses?: Array<() => Promise>; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + payload?: Record; + params: { + name: string; + }; +} - it('returns the cluster information from Elasticsearch', async () => { - const mockCreateRequest = ({ - payload: { - seeds: [], - }, - params: { - name: 'test_cluster', - }, - } as unknown) as Request; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce({ test_cluster: true }) - .mockReturnValueOnce(null) - .mockReturnValueOnce({ - acknowledged: true, - persistent: { - cluster: { - remote: { - test_cluster: { - seeds: [], - }, - }, +describe('UPDATE remote clusters', () => { + const updateRemoteClustersTest = ( + description: string, + { + licenseCheckResult = { valid: true }, + apiResponses = [], + asserts, + payload, + params, + }: TestOptions + ) => { + test(description, async () => { + const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + + const mockRouteDependencies = { + router: httpServiceMock.createRouter(), + getLicenseStatus: () => licenseCheckResult, + elasticsearchService: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, + }; + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + + elasticsearchServiceMock + .createClusterClient() + .asScoped.mockReturnValue(mockScopedClusterClient); + + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + register(mockRouteDependencies); + const [[{ validate }, handler]] = mockRouteDependencies.router.put.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'put', + path: `${API_BASE_PATH}/{name}`, + params: (validate as any).params.validate(params), + body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, + headers: { authorization: 'foo' }, + }); + + const mockContext = ({ + core: { + elasticsearch: { + dataClient: mockScopedClusterClient, }, }, - }); + } as unknown) as RequestHandlerContext; - const response = await updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit); - const expectedResponse = { - name: 'test_cluster', - seeds: [], - isConfiguredByNode: false, - }; - expect(response).toEqual(expectedResponse); - }); + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); - it(`throws an error if the response doesn't contain cluster information`, async () => { - const mockCreateRequest = ({ + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + }); + }; + + describe('success', () => { + updateRemoteClustersTest('updates remote cluster', { + apiResponses: [ + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + async () => ({ + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: true, + }, + }, + }, + }, + transient: {}, + }), + ], + params: { + name: 'test', + }, payload: { - seeds: [], + seeds: ['127.0.0.1:9300'], + skipUnavailable: true, }, - params: { - name: 'test_cluster', + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: true } }, + }, + }, + }, + }, + ], + ], + statusCode: 200, + result: { + connectedNodesCount: 1, + initialConnectTimeout: '30s', + isConfiguredByNode: false, + isConnected: true, + maxConnectionsPerCluster: 3, + name: 'test', + seeds: ['127.0.0.1:9300'], + skipUnavailable: true, + }, }, - } as unknown) as Request; - - const callWithRequest = jest - .fn() - .mockReturnValueOnce({ test_cluster: true }) - .mockReturnValueOnce({ - acknowledged: true, - persistent: {}, - }); - - const expectedError = wrapCustomError( - new Error('Unable to update cluster, no response returned from ES.'), - 400 - ); - await expect( - updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) - ).rejects.toThrow(expectedError); + }); }); - it('throws an error if the cluster does not exist', async () => { - const mockCreateRequest = ({ + describe('failure', () => { + updateRemoteClustersTest('returns 404 if remote cluster does not exist', { + apiResponses: [async () => ({})], payload: { - seeds: [], + seeds: ['127.0.0.1:9300'], + skipUnavailable: false, }, params: { - name: 'test_cluster', + name: 'test', }, - } as unknown) as Request; - - const callWithRequest = jest.fn().mockReturnValueOnce({}); - - const expectedError = wrapCustomError( - new Error('There is no remote cluster with that name.'), - 404 - ); - await expect( - updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) - ).rejects.toThrow(expectedError); - }); + asserts: { + apiArguments: [['cluster.remoteInfo']], + statusCode: 404, + result: { + message: 'There is no remote cluster with that name.', + }, + }, + }); - it('throws an ES error when one is received', async () => { - const mockCreateRequest = ({ + updateRemoteClustersTest('returns 400 if ES did not acknowledge remote cluster', { + apiResponses: [ + async () => ({ + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }), + async () => ({}), + ], payload: { - seeds: [], + seeds: ['127.0.0.1:9300'], + skipUnavailable: false, }, params: { - name: 'test_cluster', + name: 'test', }, - } as unknown) as Request; - - const mockError = new Error() as any; - mockError.response = JSON.stringify({ error: 'Test error' }); - - const callWithRequest = jest - .fn() - .mockReturnValueOnce({ test_cluster: true }) - .mockRejectedValueOnce(mockError); - - await expect( - updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) - ).rejects.toThrow(mockError); + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + }, + }, + }, + }, + ], + ], + statusCode: 400, + result: { + message: 'Unable to edit cluster, no response returned from ES.', + }, + }, + }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 2d6e7ad89cb6e9..3b0c0186a18a9c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -15,19 +15,18 @@ import { doesClusterExist } from '../../lib/does_cluster_exist'; import { RouteDependencies } from '../../types'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; export const register = (deps: RouteDependencies): void => { // TODO there are other settings that can be specified for a remote cluster via console/API that I think might cause issues when editing const updateHandler: RequestHandler = async (ctx, request, response) => { try { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; const { name } = request.params; const { seeds, skipUnavailable } = request.body; // Check if cluster does exist. - const existingCluster = await doesClusterExist(callWithRequest, name); + const existingCluster = await doesClusterExist(callAsCurrentUser, name); if (!existingCluster) { return response.customError({ statusCode: 404, @@ -42,15 +41,9 @@ export const register = (deps: RouteDependencies): void => { }); } - // TODO is this still needed? Issue is closed - // Delete existing cluster settings. - // This is a workaround for: https://github.com/elastic/elasticsearch/issues/37799 - const deleteClusterPayload = serializeCluster({ name }); - await callWithRequest('cluster.putSettings', { body: deleteClusterPayload }); - // Update cluster as new settings const updateClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); - const updateClusterResponse = await callWithRequest('cluster.putSettings', { + const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body: updateClusterPayload, }); From 18a36f3094f65bb748071495848af079f05c0bb1 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 6 Feb 2020 22:16:09 -0500 Subject: [PATCH 4/8] fix TS --- .../license_pre_routing_factory.test.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts index dcb6cc536ba7a2..ff777698599cf3 100644 --- a/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts +++ b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.ts @@ -7,21 +7,29 @@ import expect from '@kbn/expect'; import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { licensePreRoutingFactory } from '../license_pre_routing_factory'; +import { LicenseStatus } from '../../types'; describe('licensePreRoutingFactory()', () => { - let mockDeps; - let licenseStatus; + let mockDeps: any; + let mockContext: any; + let licenseStatus: LicenseStatus; beforeEach(() => { mockDeps = { getLicenseStatus: () => licenseStatus }; + mockContext = { + core: {}, + actions: {}, + licensing: {}, + }; }); describe('status is not valid', () => { it('replies with 403', () => { licenseStatus = { valid: false }; - const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => {}); - const stubRequest = {}; - const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); + const stubRequest: any = {}; + const stubHandler: any = () => {}; + const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, stubHandler); + const response: any = routeWithLicenseCheck(mockContext, stubRequest, kibanaResponseFactory); expect(response.status).to.be(403); }); }); @@ -29,9 +37,10 @@ describe('licensePreRoutingFactory()', () => { describe('status is valid', () => { it('replies with nothing', () => { licenseStatus = { valid: true }; - const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => null); - const stubRequest = {}; - const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); + const stubRequest: any = {}; + const stubHandler: any = () => null; + const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, stubHandler); + const response = routeWithLicenseCheck(mockContext, stubRequest, kibanaResponseFactory); expect(response).to.be(null); }); }); From 12efed38fab9605d006d1a81ab10d531e8d916df Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 6 Feb 2020 22:44:33 -0500 Subject: [PATCH 5/8] update api integrationt tests --- .../public/app/store/actions/remove_clusters.js | 4 +--- .../management/remote_clusters/remote_clusters.js | 15 +++++---------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js index e41c10e001be47..4086a91e290212 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js @@ -63,9 +63,7 @@ export const removeClusters = names => async (dispatch, getState) => { const { name, error: { - output: { - payload: { msg: message }, - }, + payload: { message }, }, } = errors[0]; diff --git a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js index 947e28cf111534..677d22ff749847 100644 --- a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js +++ b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js @@ -57,6 +57,7 @@ export default function({ getService }) { .send({ name: 'test_cluster', seeds: [NODE_SEED], + skipUnavailable: false, }) .expect(409); @@ -183,17 +184,11 @@ export default function({ getService }) { { name: 'test_cluster_doesnt_exist', error: { - isBoom: true, - isServer: false, - data: null, - output: { + status: 404, + payload: { message: 'There is no remote cluster with that name.' }, + options: { statusCode: 404, - payload: { - statusCode: 404, - error: 'Not Found', - message: 'There is no remote cluster with that name.', - }, - headers: {}, + body: { message: 'There is no remote cluster with that name.' }, }, }, }, From c9d0c5db05abfa3fdebe8b14129feb239a38e82f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 6 Feb 2020 22:55:41 -0500 Subject: [PATCH 6/8] remove todo comment --- x-pack/plugins/remote_clusters/server/routes/api/update_route.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 3b0c0186a18a9c..bcd4337899232c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -17,7 +17,6 @@ import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory' import { isEsError } from '../../lib/is_es_error'; export const register = (deps: RouteDependencies): void => { - // TODO there are other settings that can be specified for a remote cluster via console/API that I think might cause issues when editing const updateHandler: RequestHandler = async (ctx, request, response) => { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; From 14cbcb6cb1825fc89c6907a0d34b3231956158c6 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 7 Feb 2020 08:28:17 -0500 Subject: [PATCH 7/8] i18n fix --- x-pack/.i18nrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 68f4498ff23748..27da54042594d1 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -27,7 +27,7 @@ "xpack.maps": "legacy/plugins/maps", "xpack.ml": "legacy/plugins/ml", "xpack.monitoring": "legacy/plugins/monitoring", - "xpack.remoteClusters": "legacy/plugins/remote_clusters", + "xpack.remoteClusters": ["plugins/remote_clusters", "legacy/plugins/remote_clusters"], "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "plugins/searchprofiler", From 276389aa000ac85544efbbdd10b5540497a8628a Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 10 Feb 2020 10:29:18 -0500 Subject: [PATCH 8/8] addres review feedback --- .../license_pre_routing_factory.ts | 2 +- .../server/routes/api/add_route.ts | 22 +++++++++----- .../server/routes/api/delete_route.ts | 19 ++++++++---- .../server/routes/api/get_route.ts | 2 +- .../server/routes/api/update_route.ts | 30 +++++++++++++------ 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts index 481de968e661c5..09d78302a7e76f 100644 --- a/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ b/x-pack/plugins/remote_clusters/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -14,7 +14,7 @@ import { RouteDependencies } from '../../types'; export const licensePreRoutingFactory = ( { getLicenseStatus }: RouteDependencies, - handler: RequestHandler + handler: RequestHandler ) => { return function licenseCheck( ctx: RequestHandlerContext, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 8fbc964f9a7306..aa09b6bf456677 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; @@ -16,8 +16,20 @@ import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory' import { isEsError } from '../../lib/is_es_error'; import { RouteDependencies } from '../../types'; +const bodyValidation = schema.object({ + name: schema.string(), + seeds: schema.arrayOf(schema.string()), + skipUnavailable: schema.boolean(), +}); + +type RouteBody = TypeOf; + export const register = (deps: RouteDependencies): void => { - const addHandler: RequestHandler = async (ctx, request, response) => { + const addHandler: RequestHandler = async ( + ctx, + request, + response + ) => { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; @@ -78,11 +90,7 @@ export const register = (deps: RouteDependencies): void => { { path: API_BASE_PATH, validate: { - body: schema.object({ - name: schema.string(), - seeds: schema.arrayOf(schema.string()), - skipUnavailable: schema.boolean(), - }), + body: bodyValidation, }, }, licensePreRoutingFactory(deps, addHandler) diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index 0e1f06f483aca7..742780ffed309e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; @@ -16,8 +16,18 @@ import { doesClusterExist } from '../../lib/does_cluster_exist'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; +const paramsValidation = schema.object({ + nameOrNames: schema.string(), +}); + +type RouteParams = TypeOf; + export const register = (deps: RouteDependencies): void => { - const deleteHandler: RequestHandler = async (ctx, request, response) => { + const deleteHandler: RequestHandler = async ( + ctx, + request, + response + ) => { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; @@ -57,6 +67,7 @@ export const register = (deps: RouteDependencies): void => { const acknowledged = get(updateClusterResponse, 'acknowledged'); const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); + // Deletion was successful if (acknowledged && !cluster) { return null; } @@ -119,9 +130,7 @@ export const register = (deps: RouteDependencies): void => { { path: `${API_BASE_PATH}/{nameOrNames}`, validate: { - params: schema.object({ - nameOrNames: schema.string(), - }), + params: paramsValidation, }, }, licensePreRoutingFactory(deps, deleteHandler) diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index 46b3eb74c0ff12..44b6284109ac54 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -14,7 +14,7 @@ import { isEsError } from '../../lib/is_es_error'; import { RouteDependencies } from '../../types'; export const register = (deps: RouteDependencies): void => { - const allHandler: RequestHandler = async (ctx, request, response) => { + const allHandler: RequestHandler = async (ctx, request, response) => { try { const callAsCurrentUser = await ctx.core.elasticsearch.dataClient.callAsCurrentUser; const clusterSettings = await callAsCurrentUser('cluster.getSettings'); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index bcd4337899232c..fd707f15ad11ed 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; @@ -16,8 +16,25 @@ import { RouteDependencies } from '../../types'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; +const bodyValidation = schema.object({ + seeds: schema.arrayOf(schema.string()), + skipUnavailable: schema.boolean(), +}); + +const paramsValidation = schema.object({ + name: schema.string(), +}); + +type RouteParams = TypeOf; + +type RouteBody = TypeOf; + export const register = (deps: RouteDependencies): void => { - const updateHandler: RequestHandler = async (ctx, request, response) => { + const updateHandler: RequestHandler = async ( + ctx, + request, + response + ) => { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; @@ -82,13 +99,8 @@ export const register = (deps: RouteDependencies): void => { { path: `${API_BASE_PATH}/{name}`, validate: { - params: schema.object({ - name: schema.string(), - }), - body: schema.object({ - seeds: schema.arrayOf(schema.string()), - skipUnavailable: schema.boolean(), - }), + params: paramsValidation, + body: bodyValidation, }, }, licensePreRoutingFactory(deps, updateHandler)