= {}
+): { message: string; body?: { cause?: string[] }; statusCode: number } {
const { statusCode, response } = err;
const {
error: {
- root_cause = [], // eslint-disable-line camelcase
- caused_by, // eslint-disable-line camelcase
+ root_cause = [], // eslint-disable-line @typescript-eslint/camelcase
+ caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase
} = {},
} = JSON.parse(response);
// If no custom message if specified for the error's status code, just
// wrap the error as a Boom error response and return it
if (!statusCodeToMessageMap[statusCode]) {
- const boomError = Boom.boomify(err, { statusCode });
-
// The caused_by chain has the most information so use that if it's available. If not then
// settle for the root_cause.
const causedByChain = extractCausedByChain(caused_by);
const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined;
- boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause;
- return boomError;
+ return {
+ message: err.message,
+ statusCode,
+ body: {
+ cause: causedByChain.length ? causedByChain : defaultCause,
+ },
+ };
}
// Otherwise, use the custom message to create a Boom error response and
// return it
const message = statusCodeToMessageMap[statusCode];
- return new Boom(message, { statusCode });
+ return { message, statusCode };
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts
similarity index 50%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_unknown_error.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts
index ffd915c5133626..4137293cf39c06 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_unknown_error.js
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts
@@ -4,14 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
+import * as legacyElasticsearch from 'elasticsearch';
-/**
- * Wraps an unknown error into a Boom error response and returns it
- *
- * @param err Object Unknown error
- * @return Object Boom error response
- */
-export function wrapUnknownError(err) {
- return Boom.boomify(err);
+const esErrorsParent = legacyElasticsearch.errors._Abstract;
+
+export function isEsError(err: Error) {
+ return err instanceof esErrorsParent;
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/index.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts
similarity index 76%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/is_es_error_factory.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts
index 6c17554385ef85..fc6405b8e75133 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/is_es_error_factory.js
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts
@@ -6,13 +6,13 @@
import { memoize } from 'lodash';
-const esErrorsFactory = memoize(server => {
+const esErrorsFactory = memoize((server: any) => {
return server.plugins.elasticsearch.getCluster('admin').errors;
});
-export function isEsErrorFactory(server) {
+export function isEsErrorFactory(server: any) {
const esErrors = esErrorsFactory(server);
- return function isEsError(err) {
+ return function isEsError(err: any) {
return err instanceof esErrors._Abstract;
};
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts
new file mode 100644
index 00000000000000..d22505f0e315ad
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server';
+import { licensePreRoutingFactory } from '../license_pre_routing_factory';
+
+describe('license_pre_routing_factory', () => {
+ describe('#reportingFeaturePreRoutingFactory', () => {
+ let mockDeps: any;
+ let mockLicenseCheckResults: any;
+
+ const anyContext: any = {};
+ const anyRequest: any = {};
+
+ beforeEach(() => {
+ mockDeps = {
+ __LEGACY: {
+ server: {
+ plugins: {
+ xpack_main: {
+ info: {
+ feature: () => ({
+ getLicenseCheckResults: () => mockLicenseCheckResults,
+ }),
+ },
+ },
+ },
+ },
+ },
+ requestHandler: jest.fn(),
+ };
+ });
+
+ describe('isAvailable is false', () => {
+ beforeEach(() => {
+ mockLicenseCheckResults = {
+ isAvailable: false,
+ };
+ });
+
+ it('replies with 403', async () => {
+ const licensePreRouting = licensePreRoutingFactory(mockDeps);
+ const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory);
+ expect(response.status).toBe(403);
+ });
+ });
+
+ describe('isAvailable is true', () => {
+ beforeEach(() => {
+ mockLicenseCheckResults = {
+ isAvailable: true,
+ };
+ });
+
+ it('it calls the wrapped handler', async () => {
+ const licensePreRouting = licensePreRoutingFactory(mockDeps);
+ await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory);
+ expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/index.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts
new file mode 100644
index 00000000000000..c47faa940a6503
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { RequestHandler } from 'src/core/server';
+import { PLUGIN } from '../../../../common/constants';
+
+export const licensePreRoutingFactory = ({
+ __LEGACY,
+ requestHandler,
+}: {
+ __LEGACY: { server: any };
+ requestHandler: RequestHandler
;
+}) => {
+ const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
+
+ // License checking and enable/disable logic
+ const licensePreRouting: RequestHandler
= (ctx, request, response) => {
+ const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
+ if (!licenseCheckResults.isAvailable) {
+ return response.forbidden({
+ body: licenseCheckResults.message,
+ });
+ } else {
+ return requestHandler(ctx, request, response);
+ }
+ };
+
+ return licensePreRouting;
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js
similarity index 100%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/index.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js
similarity index 66%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/register_license_checker.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js
index dbd99efd955734..b9bb34a80ce796 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/register_license_checker.js
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status';
-import { PLUGIN } from '../../../common/constants';
+import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status';
+import { PLUGIN } from '../../../../common/constants';
import { checkLicense } from '../check_license';
-export function registerLicenseChecker(server) {
- const xpackMainPlugin = server.plugins.xpack_main;
- const ccrPluggin = server.plugins[PLUGIN.ID];
+export function registerLicenseChecker(__LEGACY) {
+ const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
+ const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID];
mirrorPluginStatus(xpackMainPlugin, ccrPluggin);
xpackMainPlugin.status.once('green', () => {
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts
new file mode 100644
index 00000000000000..1012c07af3d2ae
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server';
+
+import { IndexMgmtSetup } from '../../../../../plugins/index_management/server';
+
+// @ts-ignore
+import { registerLicenseChecker } from './lib/register_license_checker';
+// @ts-ignore
+import { registerRoutes } from './routes/register_routes';
+import { ccrDataEnricher } from './cross_cluster_replication_data';
+
+interface PluginDependencies {
+ indexManagement: IndexMgmtSetup;
+ __LEGACY: {
+ server: any;
+ ccrUIEnabled: boolean;
+ };
+}
+
+export class CrossClusterReplicationServerPlugin implements Plugin {
+ // @ts-ignore
+ constructor(private readonly ctx: PluginInitializerContext) {}
+ setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) {
+ registerLicenseChecker(__LEGACY);
+
+ const router = http.createRouter();
+ registerRoutes({ router, __LEGACY });
+ if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) {
+ indexManagement.indexDataEnricher.add(ccrDataEnricher);
+ }
+ }
+ start() {}
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js
similarity index 68%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js
index c610039cfd2aca..f3024515c7213d 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js
@@ -3,23 +3,23 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization';
+import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures';
+import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern';
-import { deserializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization';
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../lib/is_es_error_factory';
-import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../fixtures';
-import { registerAutoFollowPatternRoutes } from './auto_follow_pattern';
+import { createRouter, callRoute } from './helpers';
-jest.mock('../../lib/call_with_request_factory');
-jest.mock('../../lib/is_es_error_factory');
-jest.mock('../../lib/license_pre_routing_factory');
+jest.mock('../../../lib/call_with_request_factory');
+jest.mock('../../../lib/is_es_error_factory');
+jest.mock('../../../lib/license_pre_routing_factory', () => ({
+ licensePreRoutingFactory: ({ requestHandler }) => requestHandler,
+}));
const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock()));
-/**
- * Hashtable to save the route handlers
- */
-const routeHandlers = {};
+let routeRegistry;
/**
* Helper to extract all the different server route handler so we can easily call them in our tests.
@@ -28,8 +28,6 @@ const routeHandlers = {};
* if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here.
*/
const registerHandlers = () => {
- let index = 0;
-
const HANDLER_INDEX_TO_ACTION = {
0: 'list',
1: 'create',
@@ -40,15 +38,12 @@ const registerHandlers = () => {
6: 'resume',
};
- const server = {
- route({ handler }) {
- // Save handler and increment index
- routeHandlers[HANDLER_INDEX_TO_ACTION[index]] = handler;
- index++;
- },
- };
+ routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION);
- registerAutoFollowPatternRoutes(server);
+ registerAutoFollowPatternRoutes({
+ __LEGACY: {},
+ router: routeRegistry.router,
+ });
};
/**
@@ -94,14 +89,16 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
describe('list()', () => {
beforeEach(() => {
- routeHandler = routeHandlers.list;
+ routeHandler = routeRegistry.getRoutes().list;
});
it('should deserialize the response from Elasticsearch', async () => {
const totalResult = 2;
setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult));
- const response = await routeHandler();
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler);
const autoFollowPattern = response.patterns[0];
expect(response.patterns.length).toEqual(totalResult);
@@ -112,21 +109,25 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
describe('create()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.create;
+ routeHandler = routeRegistry.getRoutes().create;
});
it('should throw a 409 conflict error if id already exists', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({
- payload: {
- id: 'some-id',
- foo: 'bar',
- },
- }).catch(err => err); // return the error
-
- expect(response.output.statusCode).toEqual(409);
+ const response = await callRoute(
+ routeHandler,
+ {},
+ {
+ body: {
+ id: 'some-id',
+ foo: 'bar',
+ },
+ }
+ );
+
+ expect(response.status).toEqual(409);
});
it('should return 200 status when the id does not exist', async () => {
@@ -135,12 +136,18 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(error);
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({
- payload: {
- id: 'some-id',
- foo: 'bar',
- },
- });
+ const {
+ options: { body: response },
+ } = await callRoute(
+ routeHandler,
+ {},
+ {
+ body: {
+ id: 'some-id',
+ foo: 'bar',
+ },
+ }
+ );
expect(response).toEqual({ acknowledge: true });
});
@@ -148,7 +155,7 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
describe('update()', () => {
beforeEach(() => {
- routeHandler = routeHandlers.update;
+ routeHandler = routeRegistry.getRoutes().update;
});
it('should serialize the payload before sending it to Elasticsearch', async () => {
@@ -156,16 +163,16 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
const request = {
params: { id: 'foo' },
- payload: {
+ body: {
remoteCluster: 'bar1',
leaderIndexPatterns: ['bar2'],
followIndexPattern: 'bar3',
},
};
- const response = await routeHandler(request);
+ const response = await callRoute(routeHandler, {}, request);
- expect(response).toEqual({
+ expect(response.options.body).toEqual({
id: 'foo',
body: {
remote_cluster: 'bar1',
@@ -178,7 +185,7 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
describe('get()', () => {
beforeEach(() => {
- routeHandler = routeHandlers.get;
+ routeHandler = routeRegistry.getRoutes().get;
});
it('should return a single resource even though ES return an array with 1 item', async () => {
@@ -187,21 +194,23 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, esResponse);
- const response = await routeHandler({ params: { id: 1 } });
- expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS);
+ const response = await callRoute(routeHandler, {}, { params: { id: 1 } });
+ expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS);
});
});
describe('delete()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.delete;
+ routeHandler = routeRegistry.getRoutes().delete;
});
it('should delete a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: 'a' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: 'a' } });
expect(response.itemsDeleted).toEqual(['a']);
expect(response.errors).toEqual([]);
@@ -212,9 +221,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: 'a,b,c' } });
+ const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
- expect(response.itemsDeleted).toEqual(['a', 'b', 'c']);
+ expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']);
});
it('should catch error and return them in array', async () => {
@@ -224,7 +233,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
- const response = await routeHandler({ params: { id: 'a,b' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
expect(response.itemsDeleted).toEqual(['a']);
expect(response.errors[0].id).toEqual('b');
@@ -234,13 +245,15 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
describe('pause()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.pause;
+ routeHandler = routeRegistry.getRoutes().pause;
});
it('accept a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: 'a' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: 'a' } });
expect(response.itemsPaused).toEqual(['a']);
expect(response.errors).toEqual([]);
@@ -251,9 +264,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: 'a,b,c' } });
+ const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
- expect(response.itemsPaused).toEqual(['a', 'b', 'c']);
+ expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']);
});
it('should catch error and return them in array', async () => {
@@ -263,7 +276,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
- const response = await routeHandler({ params: { id: 'a,b' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
expect(response.itemsPaused).toEqual(['a']);
expect(response.errors[0].id).toEqual('b');
@@ -273,13 +288,15 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
describe('resume()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.resume;
+ routeHandler = routeRegistry.getRoutes().resume;
});
it('accept a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: 'a' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: 'a' } });
expect(response.itemsResumed).toEqual(['a']);
expect(response.errors).toEqual([]);
@@ -290,9 +307,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: 'a,b,c' } });
+ const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
- expect(response.itemsResumed).toEqual(['a', 'b', 'c']);
+ expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']);
});
it('should catch error and return them in array', async () => {
@@ -302,7 +319,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
- const response = await routeHandler({ params: { id: 'a,b' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
expect(response.itemsResumed).toEqual(['a']);
expect(response.errors[0].id).toEqual('b');
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js
similarity index 72%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js
index 7e363c2758a4c9..f0139e5bd70115 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js
@@ -3,21 +3,23 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-import { deserializeFollowerIndex } from '../../../common/services/follower_index_serialization';
+import { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization';
import {
getFollowerIndexStatsMock,
getFollowerIndexListStatsMock,
getFollowerIndexInfoMock,
getFollowerIndexListInfoMock,
-} from '../../../fixtures';
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../lib/is_es_error_factory';
-import { registerFollowerIndexRoutes } from './follower_index';
-
-jest.mock('../../lib/call_with_request_factory');
-jest.mock('../../lib/is_es_error_factory');
-jest.mock('../../lib/license_pre_routing_factory');
+} from '../../../../../fixtures';
+import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { registerFollowerIndexRoutes } from '../follower_index';
+import { createRouter, callRoute } from './helpers';
+
+jest.mock('../../../lib/call_with_request_factory');
+jest.mock('../../../lib/is_es_error_factory');
+jest.mock('../../../lib/license_pre_routing_factory', () => ({
+ licensePreRoutingFactory: ({ requestHandler }) => requestHandler,
+}));
const DESERIALIZED_KEYS = Object.keys(
deserializeFollowerIndex({
@@ -26,10 +28,7 @@ const DESERIALIZED_KEYS = Object.keys(
})
);
-/**
- * Hashtable to save the route handlers
- */
-const routeHandlers = {};
+let routeRegistry;
/**
* Helper to extract all the different server route handler so we can easily call them in our tests.
@@ -38,8 +37,6 @@ const routeHandlers = {};
* if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here.
*/
const registerHandlers = () => {
- let index = 0;
-
const HANDLER_INDEX_TO_ACTION = {
0: 'list',
1: 'get',
@@ -50,15 +47,11 @@ const registerHandlers = () => {
6: 'unfollow',
};
- const server = {
- route({ handler }) {
- // Save handler and increment index
- routeHandlers[HANDLER_INDEX_TO_ACTION[index]] = handler;
- index++;
- },
- };
-
- registerFollowerIndexRoutes(server);
+ routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION);
+ registerFollowerIndexRoutes({
+ __LEGACY: {},
+ router: routeRegistry.router,
+ });
};
/**
@@ -104,7 +97,7 @@ describe('[CCR API Routes] Follower Index', () => {
describe('list()', () => {
beforeEach(() => {
- routeHandler = routeHandlers.list;
+ routeHandler = routeRegistry.getRoutes().list;
});
it('deserializes the response from Elasticsearch', async () => {
@@ -117,7 +110,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, infoResult);
setHttpRequestResponse(null, statsResult);
- const response = await routeHandler();
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler);
const followerIndex = response.indices[0];
expect(response.indices.length).toEqual(totalResult);
@@ -127,7 +122,7 @@ describe('[CCR API Routes] Follower Index', () => {
describe('get()', () => {
beforeEach(() => {
- routeHandler = routeHandlers.get;
+ routeHandler = routeRegistry.getRoutes().get;
});
it('should return a single resource even though ES return an array with 1 item', async () => {
@@ -138,7 +133,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] });
setHttpRequestResponse(null, { indices: [followerIndexStats] });
- const response = await routeHandler({ params: { id: mockId } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: mockId } });
expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS);
});
});
@@ -146,34 +143,40 @@ describe('[CCR API Routes] Follower Index', () => {
describe('create()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.create;
+ routeHandler = routeRegistry.getRoutes().create;
});
it('should return 200 status when follower index is created', async () => {
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({
- payload: {
- name: 'follower_index',
- remoteCluster: 'remote_cluster',
- leaderIndex: 'leader_index',
- },
- });
+ const response = await callRoute(
+ routeHandler,
+ {},
+ {
+ body: {
+ name: 'follower_index',
+ remoteCluster: 'remote_cluster',
+ leaderIndex: 'leader_index',
+ },
+ }
+ );
- expect(response).toEqual({ acknowledge: true });
+ expect(response.options.body).toEqual({ acknowledge: true });
});
});
describe('pause()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.pause;
+ routeHandler = routeRegistry.getRoutes().pause;
});
it('should pause a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: '1' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: '1' } });
expect(response.itemsPaused).toEqual(['1']);
expect(response.errors).toEqual([]);
@@ -184,9 +187,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: '1,2,3' } });
+ const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
- expect(response.itemsPaused).toEqual(['1', '2', '3']);
+ expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']);
});
it('should catch error and return them in array', async () => {
@@ -196,7 +199,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
- const response = await routeHandler({ params: { id: '1,2' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
expect(response.itemsPaused).toEqual(['1']);
expect(response.errors[0].id).toEqual('2');
@@ -206,13 +211,15 @@ describe('[CCR API Routes] Follower Index', () => {
describe('resume()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.resume;
+ routeHandler = routeRegistry.getRoutes().resume;
});
it('should resume a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: '1' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: '1' } });
expect(response.itemsResumed).toEqual(['1']);
expect(response.errors).toEqual([]);
@@ -223,9 +230,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: '1,2,3' } });
+ const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
- expect(response.itemsResumed).toEqual(['1', '2', '3']);
+ expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']);
});
it('should catch error and return them in array', async () => {
@@ -235,7 +242,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
- const response = await routeHandler({ params: { id: '1,2' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
expect(response.itemsResumed).toEqual(['1']);
expect(response.errors[0].id).toEqual('2');
@@ -245,7 +254,7 @@ describe('[CCR API Routes] Follower Index', () => {
describe('unfollow()', () => {
beforeEach(() => {
resetHttpRequestResponses();
- routeHandler = routeHandlers.unfollow;
+ routeHandler = routeRegistry.getRoutes().unfollow;
});
it('should unfollow await single item', async () => {
@@ -254,7 +263,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: '1' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: '1' } });
expect(response.itemsUnfollowed).toEqual(['1']);
expect(response.errors).toEqual([]);
@@ -274,9 +285,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
- const response = await routeHandler({ params: { id: '1,2,3' } });
+ const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
- expect(response.itemsUnfollowed).toEqual(['1', '2', '3']);
+ expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']);
});
it('should catch error and return them in array', async () => {
@@ -290,7 +301,9 @@ describe('[CCR API Routes] Follower Index', () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
- const response = await routeHandler({ params: { id: '1,2' } });
+ const {
+ options: { body: response },
+ } = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
expect(response.itemsUnfollowed).toEqual(['1']);
expect(response.errors[0].id).toEqual('2');
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts
new file mode 100644
index 00000000000000..555fc0937c0ad6
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { RequestHandler } from 'src/core/server';
+import { kibanaResponseFactory } from '../../../../../../../../../src/core/server';
+
+export const callRoute = (
+ route: RequestHandler,
+ ctx = {},
+ request = {},
+ response = kibanaResponseFactory
+) => {
+ return route(ctx as any, request as any, response);
+};
+
+export const createRouter = (indexToActionMap: Record) => {
+ let index = 0;
+ const routeHandlers: Record> = {};
+ const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => {
+ // Save handler and increment index
+ routeHandlers[indexToActionMap[index]] = handler;
+ index++;
+ };
+
+ return {
+ getRoutes: () => routeHandlers,
+ router: {
+ get: addHandler,
+ post: addHandler,
+ put: addHandler,
+ delete: addHandler,
+ },
+ };
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts
new file mode 100644
index 00000000000000..d458f1ccb354b6
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts
@@ -0,0 +1,301 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { schema } from '@kbn/config-schema';
+// @ts-ignore
+import { callWithRequestFactory } from '../../lib/call_with_request_factory';
+import { isEsError } from '../../lib/is_es_error';
+// @ts-ignore
+import {
+ deserializeAutoFollowPattern,
+ deserializeListAutoFollowPatterns,
+ serializeAutoFollowPattern,
+ // @ts-ignore
+} from '../../../../common/services/auto_follow_pattern_serialization';
+
+import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
+import { API_BASE_PATH } from '../../../../common/constants';
+
+import { RouteDependencies } from '../types';
+import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
+
+export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => {
+ /**
+ * Returns a list of all auto-follow patterns
+ */
+ router.get(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns`,
+ validate: false,
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+
+ try {
+ const result = await callWithRequest('ccr.autoFollowPatterns');
+ return response.ok({
+ body: {
+ patterns: deserializeListAutoFollowPatterns(result.patterns),
+ },
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Create an auto-follow pattern
+ */
+ router.post(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns`,
+ validate: {
+ body: schema.object(
+ {
+ id: schema.string(),
+ },
+ { unknowns: 'allow' }
+ ),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id, ...rest } = request.body;
+ const body = serializeAutoFollowPattern(rest);
+
+ /**
+ * First let's make sur that an auto-follow pattern with
+ * the same id does not exist.
+ */
+ try {
+ await callWithRequest('ccr.autoFollowPattern', { id });
+ // If we get here it means that an auto-follow pattern with the same id exists
+ return response.conflict({
+ body: `An auto-follow pattern with the name "${id}" already exists.`,
+ });
+ } catch (err) {
+ if (err.statusCode !== 404) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ }
+
+ try {
+ return response.ok({
+ body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }),
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Update an auto-follow pattern
+ */
+ router.put(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ body: schema.object({}, { unknowns: 'allow' }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const body = serializeAutoFollowPattern(request.body);
+
+ try {
+ return response.ok({
+ body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }),
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Returns a single auto-follow pattern
+ */
+ router.get(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+
+ try {
+ const result = await callWithRequest('ccr.autoFollowPattern', { id });
+ const autoFollowPattern = result.patterns[0];
+
+ return response.ok({
+ body: deserializeAutoFollowPattern(autoFollowPattern),
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Delete an auto-follow pattern
+ */
+ router.delete(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsDeleted: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ await Promise.all(
+ ids.map(_id =>
+ callWithRequest('ccr.deleteAutoFollowPattern', { id: _id })
+ .then(() => itemsDeleted.push(_id))
+ .catch((err: Error) => {
+ if (isEsError(err)) {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ } else {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ }
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsDeleted,
+ errors,
+ },
+ });
+ },
+ })
+ );
+
+ /**
+ * Pause auto-follow pattern(s)
+ */
+ router.post(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`,
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsPaused: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ await Promise.all(
+ ids.map(_id =>
+ callWithRequest('ccr.pauseAutoFollowPattern', { id: _id })
+ .then(() => itemsPaused.push(_id))
+ .catch((err: Error) => {
+ if (isEsError(err)) {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ } else {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ }
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsPaused,
+ errors,
+ },
+ });
+ },
+ })
+ );
+
+ /**
+ * Resume auto-follow pattern(s)
+ */
+ router.post(
+ {
+ path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`,
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsResumed: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ await Promise.all(
+ ids.map(_id =>
+ callWithRequest('ccr.resumeAutoFollowPattern', { id: _id })
+ .then(() => itemsResumed.push(_id))
+ .catch((err: Error) => {
+ if (isEsError(err)) {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ } else {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ }
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsResumed,
+ errors,
+ },
+ });
+ },
+ })
+ );
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts
new file mode 100644
index 00000000000000..b08b056ad2c8ae
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts
@@ -0,0 +1,112 @@
+/*
+ * 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/constants';
+// @ts-ignore
+import { callWithRequestFactory } from '../../lib/call_with_request_factory';
+// @ts-ignore
+import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization';
+import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
+
+import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
+import { RouteDependencies } from '../types';
+
+export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => {
+ /**
+ * Returns Auto-follow stats
+ */
+ router.get(
+ {
+ path: `${API_BASE_PATH}/stats/auto_follow`,
+ validate: false,
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+
+ try {
+ const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats');
+
+ return response.ok({
+ body: deserializeAutoFollowStats(autoFollowStats),
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Returns whether the user has CCR permissions
+ */
+ router.get(
+ {
+ path: `${API_BASE_PATH}/permissions`,
+ validate: false,
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
+ const xpackInfo = xpackMainPlugin && xpackMainPlugin.info;
+
+ if (!xpackInfo) {
+ // xpackInfo is updated via poll, so it may not be available until polling has begun.
+ // In this rare situation, tell the client the service is temporarily unavailable.
+ return response.customError({
+ statusCode: 503,
+ body: 'Security info unavailable',
+ });
+ }
+
+ const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
+ if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
+ // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR.
+ return response.ok({
+ body: {
+ hasPermission: true,
+ missingClusterPrivileges: [],
+ },
+ });
+ }
+
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+
+ try {
+ const { has_all_requested: hasPermission, cluster } = await callWithRequest(
+ 'ccr.permissions',
+ {
+ body: {
+ cluster: ['manage', 'manage_ccr'],
+ },
+ }
+ );
+
+ const missingClusterPrivileges = Object.keys(cluster).reduce(
+ (permissions: any, permissionName: any) => {
+ if (!cluster[permissionName]) {
+ permissions.push(permissionName);
+ return permissions;
+ }
+ },
+ [] as any[]
+ );
+
+ return response.ok({
+ body: {
+ hasPermission,
+ missingClusterPrivileges,
+ },
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts
new file mode 100644
index 00000000000000..3896e1c02c9150
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts
@@ -0,0 +1,345 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { schema } from '@kbn/config-schema';
+import {
+ deserializeFollowerIndex,
+ deserializeListFollowerIndices,
+ serializeFollowerIndex,
+ serializeAdvancedSettings,
+ // @ts-ignore
+} from '../../../../common/services/follower_index_serialization';
+import { API_BASE_PATH } from '../../../../common/constants';
+// @ts-ignore
+import { removeEmptyFields } from '../../../../common/services/utils';
+// @ts-ignore
+import { callWithRequestFactory } from '../../lib/call_with_request_factory';
+import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
+
+import { RouteDependencies } from '../types';
+import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
+
+export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => {
+ /**
+ * Returns a list of all follower indices
+ */
+ router.get(
+ {
+ path: `${API_BASE_PATH}/follower_indices`,
+ validate: false,
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+
+ try {
+ const { follower_indices: followerIndices } = await callWithRequest('ccr.info', {
+ id: '_all',
+ });
+
+ const {
+ follow_stats: { indices: followerIndicesStats },
+ } = await callWithRequest('ccr.stats');
+
+ const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
+ map[stats.index] = stats;
+ return map;
+ }, {});
+
+ const collatedFollowerIndices = followerIndices.map((followerIndex: any) => {
+ return {
+ ...followerIndex,
+ ...followerIndicesStatsMap[followerIndex.follower_index],
+ };
+ });
+
+ return response.ok({
+ body: {
+ indices: deserializeListFollowerIndices(collatedFollowerIndices),
+ },
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Returns a single follower index pattern
+ */
+ router.get(
+ {
+ path: `${API_BASE_PATH}/follower_indices/{id}`,
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+
+ try {
+ const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
+
+ const followerIndexInfo = followerIndices && followerIndices[0];
+
+ if (!followerIndexInfo) {
+ return response.notFound({
+ body: `The follower index "${id}" does not exist.`,
+ });
+ }
+
+ // If this follower is paused, skip call to ES stats api since it will return 404
+ if (followerIndexInfo.status === 'paused') {
+ return response.ok({
+ body: deserializeFollowerIndex({
+ ...followerIndexInfo,
+ }),
+ });
+ } else {
+ const {
+ indices: followerIndicesStats,
+ } = await callWithRequest('ccr.followerIndexStats', { id });
+
+ return response.ok({
+ body: deserializeFollowerIndex({
+ ...followerIndexInfo,
+ ...(followerIndicesStats ? followerIndicesStats[0] : {}),
+ }),
+ });
+ }
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Create a follower index
+ */
+ router.post(
+ {
+ path: `${API_BASE_PATH}/follower_indices`,
+ validate: {
+ body: schema.object(
+ {
+ name: schema.string(),
+ },
+ { unknowns: 'allow' }
+ ),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { name, ...rest } = request.body;
+ const body = removeEmptyFields(serializeFollowerIndex(rest));
+
+ try {
+ return response.ok({
+ body: await callWithRequest('ccr.saveFollowerIndex', { name, body }),
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Edit a follower index
+ */
+ router.put(
+ {
+ path: `${API_BASE_PATH}/follower_indices/{id}`,
+ validate: {
+ params: schema.object({ id: schema.string() }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+
+ // We need to first pause the follower and then resume it passing the advanced settings
+ try {
+ const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
+ const followerIndexInfo = followerIndices && followerIndices[0];
+ if (!followerIndexInfo) {
+ return response.notFound({ body: `The follower index "${id}" does not exist.` });
+ }
+
+ // Retrieve paused state instead of pulling it from the payload to ensure it's not stale.
+ const isPaused = followerIndexInfo.status === 'paused';
+ // Pause follower if not already paused
+ if (!isPaused) {
+ await callWithRequest('ccr.pauseFollowerIndex', { id });
+ }
+
+ // Resume follower
+ const body = removeEmptyFields(serializeAdvancedSettings(request.body));
+ return response.ok({
+ body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }),
+ });
+ } catch (err) {
+ return mapErrorToKibanaHttpResponse(err);
+ }
+ },
+ })
+ );
+
+ /**
+ * Pauses a follower index
+ */
+ router.put(
+ {
+ path: `${API_BASE_PATH}/follower_indices/{id}/pause`,
+ validate: {
+ params: schema.object({ id: schema.string() }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsPaused: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ await Promise.all(
+ ids.map(_id =>
+ callWithRequest('ccr.pauseFollowerIndex', { id: _id })
+ .then(() => itemsPaused.push(_id))
+ .catch((err: Error) => {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsPaused,
+ errors,
+ },
+ });
+ },
+ })
+ );
+
+ /**
+ * Resumes a follower index
+ */
+ router.put(
+ {
+ path: `${API_BASE_PATH}/follower_indices/{id}/resume`,
+ validate: {
+ params: schema.object({ id: schema.string() }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsResumed: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ await Promise.all(
+ ids.map(_id =>
+ callWithRequest('ccr.resumeFollowerIndex', { id: _id })
+ .then(() => itemsResumed.push(_id))
+ .catch((err: Error) => {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ })
+ )
+ );
+
+ return response.ok({
+ body: {
+ itemsResumed,
+ errors,
+ },
+ });
+ },
+ })
+ );
+
+ /**
+ * Unfollow follower index's leader index
+ */
+ router.put(
+ {
+ path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`,
+ validate: {
+ params: schema.object({ id: schema.string() }),
+ },
+ },
+ licensePreRoutingFactory({
+ __LEGACY,
+ requestHandler: async (ctx, request, response) => {
+ const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsUnfollowed: string[] = [];
+ const itemsNotOpen: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ await Promise.all(
+ ids.map(async _id => {
+ try {
+ // Try to pause follower, let it fail silently since it may already be paused
+ try {
+ await callWithRequest('ccr.pauseFollowerIndex', { id: _id });
+ } catch (e) {
+ // Swallow errors
+ }
+
+ // Close index
+ await callWithRequest('indices.close', { index: _id });
+
+ // Unfollow leader
+ await callWithRequest('ccr.unfollowLeaderIndex', { id: _id });
+
+ // Try to re-open the index, store failures in a separate array to surface warnings in the UI
+ // This will allow users to query their index normally after unfollowing
+ try {
+ await callWithRequest('indices.open', { index: _id });
+ } catch (e) {
+ itemsNotOpen.push(_id);
+ }
+
+ // Push success
+ itemsUnfollowed.push(_id);
+ } catch (err) {
+ errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
+ }
+ })
+ );
+
+ return response.ok({
+ body: {
+ itemsUnfollowed,
+ itemsNotOpen,
+ errors,
+ },
+ });
+ },
+ })
+ );
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts
new file mode 100644
index 00000000000000..6a81bd26dc47df
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.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 { kibanaResponseFactory } from '../../../../../../../src/core/server';
+// @ts-ignore
+import { wrapEsError } from '../lib/error_wrappers';
+import { isEsError } from '../lib/is_es_error';
+
+export const mapErrorToKibanaHttpResponse = (err: any) => {
+ if (isEsError(err)) {
+ const { statusCode, message, body } = wrapEsError(err);
+ return kibanaResponseFactory.customError({
+ statusCode,
+ body: {
+ message,
+ attributes: {
+ cause: body?.cause,
+ },
+ },
+ });
+ }
+ return kibanaResponseFactory.internalError(err);
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
similarity index 67%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/routes/register_routes.js
rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
index 6e4088ec8600f3..7e594175506918 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/register_routes.js
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
@@ -7,9 +7,10 @@
import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern';
import { registerFollowerIndexRoutes } from './api/follower_index';
import { registerCcrRoutes } from './api/ccr';
+import { RouteDependencies } from './types';
-export function registerRoutes(server) {
- registerAutoFollowPatternRoutes(server);
- registerFollowerIndexRoutes(server);
- registerCcrRoutes(server);
+export function registerRoutes(deps: RouteDependencies) {
+ registerAutoFollowPatternRoutes(deps);
+ registerFollowerIndexRoutes(deps);
+ registerCcrRoutes(deps);
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts
new file mode 100644
index 00000000000000..7f57c20c536e03
--- /dev/null
+++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.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 { IRouter } from 'src/core/server';
+
+export interface RouteDependencies {
+ router: IRouter;
+ __LEGACY: {
+ server: any;
+ };
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js
deleted file mode 100644
index 4667f0a110c1fd..00000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js
+++ /dev/null
@@ -1,256 +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 Boom from 'boom';
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../lib/is_es_error_factory';
-import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
-import {
- deserializeAutoFollowPattern,
- deserializeListAutoFollowPatterns,
- serializeAutoFollowPattern,
-} from '../../../common/services/auto_follow_pattern_serialization';
-import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
-import { API_BASE_PATH } from '../../../common/constants';
-
-export const registerAutoFollowPatternRoutes = server => {
- const isEsError = isEsErrorFactory(server);
- const licensePreRouting = licensePreRoutingFactory(server);
-
- /**
- * Returns a list of all auto-follow patterns
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns`,
- method: 'GET',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
-
- try {
- const response = await callWithRequest('ccr.autoFollowPatterns');
- return {
- patterns: deserializeListAutoFollowPatterns(response.patterns),
- };
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Create an auto-follow pattern
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns`,
- method: 'POST',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id, ...rest } = request.payload;
- const body = serializeAutoFollowPattern(rest);
-
- /**
- * First let's make sur that an auto-follow pattern with
- * the same id does not exist.
- */
- try {
- await callWithRequest('ccr.autoFollowPattern', { id });
- // If we get here it means that an auto-follow pattern with the same id exists
- const error = Boom.conflict(`An auto-follow pattern with the name "${id}" already exists.`);
- throw error;
- } catch (err) {
- if (err.statusCode !== 404) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- }
-
- try {
- return await callWithRequest('ccr.saveAutoFollowPattern', { id, body });
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Update an auto-follow pattern
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
- method: 'PUT',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const body = serializeAutoFollowPattern(request.payload);
-
- try {
- return await callWithRequest('ccr.saveAutoFollowPattern', { id, body });
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Returns a single auto-follow pattern
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
- method: 'GET',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
-
- try {
- const response = await callWithRequest('ccr.autoFollowPattern', { id });
- const autoFollowPattern = response.patterns[0];
-
- return deserializeAutoFollowPattern(autoFollowPattern);
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Delete an auto-follow pattern
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
- method: 'DELETE',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsDeleted = [];
- const errors = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.deleteAutoFollowPattern', { id: _id })
- .then(() => itemsDeleted.push(_id))
- .catch(err => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: wrapEsError(err) });
- } else {
- errors.push({ id: _id, error: wrapUnknownError(err) });
- }
- })
- )
- );
-
- return {
- itemsDeleted,
- errors,
- };
- },
- });
-
- /**
- * Pause auto-follow pattern(s)
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`,
- method: 'POST',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsPaused = [];
- const errors = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.pauseAutoFollowPattern', { id: _id })
- .then(() => itemsPaused.push(_id))
- .catch(err => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: wrapEsError(err) });
- } else {
- errors.push({ id: _id, error: wrapUnknownError(err) });
- }
- })
- )
- );
-
- return {
- itemsPaused,
- errors,
- };
- },
- });
-
- /**
- * Resume auto-follow pattern(s)
- */
- server.route({
- path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`,
- method: 'POST',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsResumed = [];
- const errors = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.resumeAutoFollowPattern', { id: _id })
- .then(() => itemsResumed.push(_id))
- .catch(err => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: wrapEsError(err) });
- } else {
- errors.push({ id: _id, error: wrapUnknownError(err) });
- }
- })
- )
- );
-
- return {
- itemsResumed,
- errors,
- };
- },
- });
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js
deleted file mode 100644
index 8255eb6e86b076..00000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js
+++ /dev/null
@@ -1,107 +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 Boom from 'boom';
-
-import { API_BASE_PATH } from '../../../common/constants';
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../lib/is_es_error_factory';
-import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
-import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization';
-import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
-
-export const registerCcrRoutes = server => {
- const isEsError = isEsErrorFactory(server);
- const licensePreRouting = licensePreRoutingFactory(server);
-
- /**
- * Returns Auto-follow stats
- */
- server.route({
- path: `${API_BASE_PATH}/stats/auto_follow`,
- method: 'GET',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
-
- try {
- const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats');
-
- return deserializeAutoFollowStats(autoFollowStats);
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Returns whether the user has CCR permissions
- */
- server.route({
- path: `${API_BASE_PATH}/permissions`,
- method: 'GET',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const xpackMainPlugin = server.plugins.xpack_main;
- const xpackInfo = xpackMainPlugin && xpackMainPlugin.info;
-
- if (!xpackInfo) {
- // xpackInfo is updated via poll, so it may not be available until polling has begun.
- // In this rare situation, tell the client the service is temporarily unavailable.
- throw new Boom('Security info unavailable', { statusCode: 503 });
- }
-
- const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
- if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
- // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR.
- return {
- hasPermission: true,
- missingClusterPrivileges: [],
- };
- }
-
- const callWithRequest = callWithRequestFactory(server, request);
-
- try {
- const { has_all_requested: hasPermission, cluster } = await callWithRequest(
- 'ccr.permissions',
- {
- body: {
- cluster: ['manage', 'manage_ccr'],
- },
- }
- );
-
- const missingClusterPrivileges = Object.keys(cluster).reduce(
- (permissions, permissionName) => {
- if (!cluster[permissionName]) {
- permissions.push(permissionName);
- return permissions;
- }
- },
- []
- );
-
- return {
- hasPermission,
- missingClusterPrivileges,
- };
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js
deleted file mode 100644
index e532edaa396366..00000000000000
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js
+++ /dev/null
@@ -1,328 +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 Boom from 'boom';
-
-import {
- deserializeFollowerIndex,
- deserializeListFollowerIndices,
- serializeFollowerIndex,
- serializeAdvancedSettings,
-} from '../../../common/services/follower_index_serialization';
-import { API_BASE_PATH } from '../../../common/constants';
-import { removeEmptyFields } from '../../../common/services/utils';
-import { callWithRequestFactory } from '../../lib/call_with_request_factory';
-import { isEsErrorFactory } from '../../lib/is_es_error_factory';
-import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
-import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
-
-export const registerFollowerIndexRoutes = server => {
- const isEsError = isEsErrorFactory(server);
- const licensePreRouting = licensePreRoutingFactory(server);
-
- /**
- * Returns a list of all follower indices
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices`,
- method: 'GET',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
-
- try {
- const { follower_indices: followerIndices } = await callWithRequest('ccr.info', {
- id: '_all',
- });
-
- const {
- follow_stats: { indices: followerIndicesStats },
- } = await callWithRequest('ccr.stats');
-
- const followerIndicesStatsMap = followerIndicesStats.reduce((map, stats) => {
- map[stats.index] = stats;
- return map;
- }, {});
-
- const collatedFollowerIndices = followerIndices.map(followerIndex => {
- return {
- ...followerIndex,
- ...followerIndicesStatsMap[followerIndex.follower_index],
- };
- });
-
- return {
- indices: deserializeListFollowerIndices(collatedFollowerIndices),
- };
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Returns a single follower index pattern
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices/{id}`,
- method: 'GET',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
-
- try {
- const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
-
- const followerIndexInfo = followerIndices && followerIndices[0];
-
- if (!followerIndexInfo) {
- const error = Boom.notFound(`The follower index "${id}" does not exist.`);
- throw error;
- }
-
- // If this follower is paused, skip call to ES stats api since it will return 404
- if (followerIndexInfo.status === 'paused') {
- return deserializeFollowerIndex({
- ...followerIndexInfo,
- });
- } else {
- const { indices: followerIndicesStats } = await callWithRequest(
- 'ccr.followerIndexStats',
- { id }
- );
-
- return deserializeFollowerIndex({
- ...followerIndexInfo,
- ...(followerIndicesStats ? followerIndicesStats[0] : {}),
- });
- }
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Create a follower index
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices`,
- method: 'POST',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { name, ...rest } = request.payload;
- const body = removeEmptyFields(serializeFollowerIndex(rest));
-
- try {
- return await callWithRequest('ccr.saveFollowerIndex', { name, body });
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Edit a follower index
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices/{id}`,
- method: 'PUT',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
-
- async function isFollowerIndexPaused() {
- const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
-
- const followerIndexInfo = followerIndices && followerIndices[0];
-
- if (!followerIndexInfo) {
- const error = Boom.notFound(`The follower index "${id}" does not exist.`);
- throw error;
- }
-
- return followerIndexInfo.status === 'paused';
- }
-
- // We need to first pause the follower and then resume it passing the advanced settings
- try {
- // Retrieve paused state instead of pulling it from the payload to ensure it's not stale.
- const isPaused = await isFollowerIndexPaused();
- // Pause follower if not already paused
- if (!isPaused) {
- await callWithRequest('ccr.pauseFollowerIndex', { id });
- }
-
- // Resume follower
- const body = removeEmptyFields(serializeAdvancedSettings(request.payload));
- return await callWithRequest('ccr.resumeFollowerIndex', { id, body });
- } catch (err) {
- if (isEsError(err)) {
- throw wrapEsError(err);
- }
- throw wrapUnknownError(err);
- }
- },
- });
-
- /**
- * Pauses a follower index
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices/{id}/pause`,
- method: 'PUT',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsPaused = [];
- const errors = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.pauseFollowerIndex', { id: _id })
- .then(() => itemsPaused.push(_id))
- .catch(err => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: wrapEsError(err) });
- } else {
- errors.push({ id: _id, error: wrapUnknownError(err) });
- }
- })
- )
- );
-
- return {
- itemsPaused,
- errors,
- };
- },
- });
-
- /**
- * Resumes a follower index
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices/{id}/resume`,
- method: 'PUT',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsResumed = [];
- const errors = [];
-
- await Promise.all(
- ids.map(_id =>
- callWithRequest('ccr.resumeFollowerIndex', { id: _id })
- .then(() => itemsResumed.push(_id))
- .catch(err => {
- if (isEsError(err)) {
- errors.push({ id: _id, error: wrapEsError(err) });
- } else {
- errors.push({ id: _id, error: wrapUnknownError(err) });
- }
- })
- )
- );
-
- return {
- itemsResumed,
- errors,
- };
- },
- });
-
- /**
- * Unfollow follower index's leader index
- */
- server.route({
- path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`,
- method: 'PUT',
- config: {
- pre: [licensePreRouting],
- },
- handler: async request => {
- const callWithRequest = callWithRequestFactory(server, request);
- const { id } = request.params;
- const ids = id.split(',');
-
- const itemsUnfollowed = [];
- const itemsNotOpen = [];
- const errors = [];
-
- await Promise.all(
- ids.map(async _id => {
- try {
- // Try to pause follower, let it fail silently since it may already be paused
- try {
- await callWithRequest('ccr.pauseFollowerIndex', { id: _id });
- } catch (e) {
- // Swallow errors
- }
-
- // Close index
- await callWithRequest('indices.close', { index: _id });
-
- // Unfollow leader
- await callWithRequest('ccr.unfollowLeaderIndex', { id: _id });
-
- // Try to re-open the index, store failures in a separate array to surface warnings in the UI
- // This will allow users to query their index normally after unfollowing
- try {
- await callWithRequest('indices.open', { index: _id });
- } catch (e) {
- itemsNotOpen.push(_id);
- }
-
- // Push success
- itemsUnfollowed.push(_id);
- } catch (err) {
- if (isEsError(err)) {
- errors.push({ id: _id, error: wrapEsError(err) });
- } else {
- errors.push({ id: _id, error: wrapUnknownError(err) });
- }
- }
- })
- );
-
- return {
- itemsUnfollowed,
- itemsNotOpen,
- errors,
- };
- },
- });
-};
diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js
index 30459be6ee1dd5..3efb4d6600f7fd 100644
--- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js
+++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js
@@ -41,7 +41,7 @@ export default function({ getService }) {
payload.remoteCluster = 'unknown-cluster';
const { body } = await createAutoFollowPattern(undefined, payload).expect(404);
- expect(body.cause[0]).to.contain('no such remote cluster');
+ expect(body.attributes.cause[0]).to.contain('no such remote cluster');
});
});
@@ -52,6 +52,7 @@ export default function({ getService }) {
it('should create an auto-follow pattern when cluster is known', async () => {
const name = getRandomString();
const { body } = await createAutoFollowPattern(name).expect(200);
+ console.log(body);
expect(body.acknowledged).to.eql(true);
});
@@ -62,7 +63,7 @@ export default function({ getService }) {
const name = getRandomString();
const { body } = await getAutoFollowPattern(name).expect(404);
- expect(body.cause).not.to.be(undefined);
+ expect(body.attributes.cause).not.to.be(undefined);
});
it('should return an auto-follow pattern that was created', async () => {
diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
index a5b12668ad9b99..5f9ebbd2a0a3fe 100644
--- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
+++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
@@ -47,13 +47,13 @@ export default function({ getService }) {
payload.remoteCluster = 'unknown-cluster';
const { body } = await createFollowerIndex(undefined, payload).expect(404);
- expect(body.cause[0]).to.contain('no such remote cluster');
+ expect(body.attributes.cause[0]).to.contain('no such remote cluster');
});
it('should throw a 404 error trying to follow an unknown index', async () => {
const payload = getFollowerIndexPayload();
const { body } = await createFollowerIndex(undefined, payload).expect(404);
- expect(body.cause[0]).to.contain('no such index');
+ expect(body.attributes.cause[0]).to.contain('no such index');
});
it('should create a follower index that follows an existing remote index', async () => {
@@ -75,7 +75,7 @@ export default function({ getService }) {
const name = getRandomString();
const { body } = await getFollowerIndex(name).expect(404);
- expect(body.cause[0]).to.contain('no such index');
+ expect(body.attributes.cause[0]).to.contain('no such index');
});
it('should return a follower index that was created', async () => {