diff --git a/x-pack/plugins/cross_cluster_replication/README.md b/x-pack/plugins/cross_cluster_replication/README.md index 9c500950c20dffd..3522499c7356aeb 100644 --- a/x-pack/plugins/cross_cluster_replication/README.md +++ b/x-pack/plugins/cross_cluster_replication/README.md @@ -4,10 +4,11 @@ You can run a local cluster and simulate a remote cluster within a single Kibana directory. -1. Start your "local" cluster by running `yarn es snapshot --license=trial` and `yarn start` to start Kibana. -2. Start your "remote" cluster by running `yarn es snapshot --license=trial -E cluster.name=europe -E transport.port=9400` in a separate terminal tab. -3. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`. -Note that these settings are required for testing auto-follow pattern conflicts errors (see below). +1. Ensure Kibana isn't running so it doesn't load up any data into your cluster. Run `yarn es snapshot --license=trial` to install a fresh snapshot. Wait for ES to finish setting up. +2. Create a "remote" copy of your ES snapshot by running: `cp -R .es/8.0.0 .es/8.0.0-2`. +4. Start your "remote" cluster by running `.es/8.0.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400`. +4. Run `yarn start` to start Kibana. +5. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`. Note that these settings are required for testing auto-follow pattern conflicts errors (see below). Now you can create follower indices and auto-follow patterns to replicate the `my-leader-index` index on the remote cluster that's available at `127.0.0.1:9400`. diff --git a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts deleted file mode 100644 index 67bdc0e998213ea..000000000000000 --- a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts +++ /dev/null @@ -1,199 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.ccr = components.clientAction.namespaceFactory(); - const ccr = Client.prototype.ccr.prototype; - - ccr.permissions = ca({ - urls: [ - { - fmt: '/_security/user/_has_privileges', - }, - ], - needBody: true, - method: 'POST', - }); - - ccr.autoFollowPatterns = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow', - }, - ], - method: 'GET', - }); - - ccr.autoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.saveAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ccr.deleteAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'DELETE', - }); - - ccr.pauseAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>/pause', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.resumeAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>/resume', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.info = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/info', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.stats = ca({ - urls: [ - { - fmt: '/_ccr/stats', - }, - ], - method: 'GET', - }); - - ccr.followerIndexStats = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/stats', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.saveFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=name%>/_ccr/follow', - req: { - name: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ccr.pauseFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/pause_follow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.resumeFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/resume_follow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ccr.unfollowLeaderIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/unfollow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts deleted file mode 100644 index a20ec5c6fd51b0d..000000000000000 --- a/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts +++ /dev/null @@ -1,78 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -function extractCausedByChain( - causedBy: Record = {}, - accumulator: string[] = [] -): string[] { - const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/naming-convention - - if (reason) { - accumulator.push(reason); - } - - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -} - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - */ -export function wrapEsError( - err: any, - statusCodeToMessageMap: Record = {} -): { message: string; body?: { cause?: string[] }; statusCode: number } { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention - caused_by = undefined, // eslint-disable-line @typescript-eslint/naming-convention - } = {}, - } = 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]) { - // 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; - - 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 { message, statusCode }; -} - -export function formatEsError(err: any): any { - const { statusCode, message, body } = wrapEsError(err); - return { - statusCode, - body: { - message, - attributes: { - cause: body?.cause, - }, - }, - }; -} diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts index e3a1de1dbfabae8..c371602df249c96 100644 --- a/x-pack/plugins/cross_cluster_replication/server/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts @@ -10,7 +10,6 @@ import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, - ILegacyCustomClusterClient, Plugin, Logger, PluginInitializerContext, @@ -19,20 +18,12 @@ import { import { Index } from '../../index_management/server'; import { PLUGIN } from '../common/constants'; -import { SetupDependencies, StartDependencies, CcrRequestHandlerContext } from './types'; +import { SetupDependencies, StartDependencies } from './types'; import { registerApiRoutes } from './routes'; -import { elasticsearchJsPlugin } from './client/elasticsearch_ccr'; import { CrossClusterReplicationConfig } from './config'; -import { License, isEsError } from './shared_imports'; -import { formatEsError } from './lib/format_es_error'; - -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - // Extend the elasticsearchJs client with additional endpoints. - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - return core.elasticsearch.legacy.createClient('crossClusterReplication', esClientConfig); -} +import { License, handleEsError } from './shared_imports'; +// TODO replace deprecated ES client after Index Management is updated const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => { if (!indicesList?.length) { return indicesList; @@ -66,7 +57,6 @@ export class CrossClusterReplicationServerPlugin implements Plugin; private readonly license: License; private readonly logger: Logger; - private ccrEsClient?: ILegacyCustomClusterClient; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); @@ -114,22 +104,11 @@ export class CrossClusterReplicationServerPlugin implements Plugin( - 'crossClusterReplication', - async (ctx, request) => { - this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.ccrEsClient.asScoped(request), - }; - } - ); - registerApiRoutes({ router: http.createRouter(), license: this.license, lib: { - isEsError, - formatEsError, + handleEsError, }, }); } @@ -142,9 +121,5 @@ export class CrossClusterReplicationServerPlugin implements Plugin { registerCreateRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,8 +33,10 @@ describe('[CCR API] Create auto-follow pattern', () => { it('should throw a 409 conflict error if id already exists', async () => { const routeContextMock = mockRouteContext({ - // Fail the uniqueness check. - callAsCurrentUser: jest.fn().mockResolvedValueOnce(true), + ccr: { + // Fail the uniqueness check. + getAutoFollowPattern: jest.fn().mockResolvedValueOnce(true), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -54,11 +52,11 @@ describe('[CCR API] Create auto-follow pattern', () => { it('should return 200 status when the id does not exist', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() + ccr: { // Pass the uniqueness check. - .mockRejectedValueOnce({ statusCode: 404 }) - .mockResolvedValueOnce(true), + getAutoFollowPattern: jest.fn().mockRejectedValueOnce({ statusCode: 404 }), + putAutoFollowPattern: jest.fn().mockResolvedValueOnce(true), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts index 608e369828de6f6..dd1481b45f87523 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types'; export const registerCreateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const bodySchema = schema.object({ id: schema.string(), @@ -34,6 +34,7 @@ export const registerCreateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id, ...rest } = request.body; const body = serializeAutoFollowPattern(rest as AutoFollowPattern); @@ -42,36 +43,29 @@ export const registerCreateRoute = ({ * the same id does not exist. */ try { - await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', { - id, - }); + await client.asCurrentUser.ccr.getAutoFollowPattern({ name: 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) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + if (error.statusCode !== 404) { + return handleEsError({ error, response }); } } try { + const { body: responseBody } = await client.asCurrentUser.ccr.putAutoFollowPattern({ + name: id, + body, + }); + return response.ok({ - body: await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.saveAutoFollowPattern', - { id, body } - ), + body: responseBody, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts index 617ef3b5af8756d..6469b4a2ce5766a 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense, mockError } from '../test_lib'; import { registerDeleteRoute } from './register_delete_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => { registerDeleteRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,7 +33,9 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => { it('deletes a single item', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + ccr: { + deleteAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -51,11 +49,13 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => { it('deletes multiple items', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + deleteAutoFollowPattern: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -69,10 +69,12 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => { it('returns partial errors', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockRejectedValueOnce({ response: { error: {} } }), + ccr: { + deleteAutoFollowPattern: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce(mockError), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts index 868e847bd6bf484..63ade6d1bd070b7 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts @@ -16,7 +16,7 @@ import { RouteDependencies } from '../../../types'; export const registerDeleteRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string(), @@ -30,29 +30,22 @@ export const registerDeleteRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const ids = id.split(','); const itemsDeleted: string[] = []; const errors: Array<{ id: string; error: any }> = []; - const formatError = (err: any) => { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - return response.customError({ statusCode: 500, body: err }); - }; - await Promise.all( ids.map((_id) => - context - .crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', { - id: _id, + client.asCurrentUser.ccr + .deleteAutoFollowPattern({ + name: _id, }) .then(() => itemsDeleted.push(_id)) - .catch((err: any) => { - errors.push({ id: _id, error: formatError(err) }); + .catch((error: any) => { + errors.push({ id: _id, error: handleEsError({ error, response }) }); }) ) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts index ec008e1507c3a1d..7b694cfe77efd36 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense } from '../test_lib'; import { registerFetchRoute } from './register_fetch_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Fetch all auto-follow patterns', () => { registerFetchRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,21 +33,25 @@ describe('[CCR API] Fetch all auto-follow patterns', () => { it('deserializes the response from Elasticsearch', async () => { const ccrAutoFollowPatternResponseMock = { - patterns: [ - { - name: 'autoFollowPattern', - pattern: { - active: true, - remote_cluster: 'remoteCluster', - leader_index_patterns: ['leader*'], - follow_index_pattern: 'follow', + body: { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, }, - }, - ], + ], + }, }; const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + ccr: { + getAutoFollowPattern: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }, }); const request = httpServerMock.createKibanaRequest(); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts index 632fdb03dd58878..98ce37d1054bca9 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerFetchRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -23,21 +23,20 @@ export const registerFetchRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; + try { - const result = await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.autoFollowPatterns' - ); + const { + body: { patterns }, + } = await client.asCurrentUser.ccr.getAutoFollowPattern(); return response.ok({ body: { - patterns: deserializeListAutoFollowPatterns(result.patterns), + // @ts-expect-error Once #98266 is merged, test this again. + patterns: deserializeListAutoFollowPatterns(patterns), }, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts index d76c77752b11555..61fdd6e57ab7abc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense } from '../test_lib'; import { registerGetRoute } from './register_get_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Get one auto-follow pattern', () => { registerGetRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,21 +33,25 @@ describe('[CCR API] Get one auto-follow pattern', () => { it('should return a single resource even though ES returns an array with 1 item', async () => { const ccrAutoFollowPatternResponseMock = { - patterns: [ - { - name: 'autoFollowPattern', - pattern: { - active: true, - remote_cluster: 'remoteCluster', - leader_index_patterns: ['leader*'], - follow_index_pattern: 'follow', + body: { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, }, - }, - ], + ], + }, }; const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + ccr: { + getAutoFollowPattern: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }, }); const request = httpServerMock.createKibanaRequest(); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts index 3529fe313dbb160..bc452982df63eff 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string(), @@ -31,24 +31,22 @@ export const registerGetRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; try { - const result = await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.autoFollowPattern', - { id } - ); - const autoFollowPattern = result.patterns[0]; + const result = await client.asCurrentUser.ccr.getAutoFollowPattern({ + name: id, + }); + + const autoFollowPattern = result.body.patterns[0]; return response.ok({ + // @ts-expect-error Once #98266 is merged, test this again. body: deserializeAutoFollowPattern(autoFollowPattern), }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts index 7a90e4086cbd704..f74da91047ce789 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense, mockError } from '../test_lib'; import { registerPauseRoute } from './register_pause_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => { registerPauseRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,7 +33,9 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => { it('pauses a single item', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + ccr: { + pauseAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -51,11 +49,13 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => { it('pauses multiple items', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + pauseAutoFollowPattern: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -69,10 +69,12 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => { it('returns partial errors', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockRejectedValueOnce({ response: { error: {} } }), + ccr: { + pauseAutoFollowPattern: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce(mockError), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts index a9aa94cdf4f2998..1428e33dee660a4 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerPauseRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string(), @@ -29,29 +29,22 @@ export const registerPauseRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const ids = id.split(','); const itemsPaused: string[] = []; const errors: Array<{ id: string; error: any }> = []; - const formatError = (err: any) => { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - return response.customError({ statusCode: 500, body: err }); - }; - await Promise.all( ids.map((_id) => - context - .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', { - id: _id, + client.asCurrentUser.ccr + .pauseAutoFollowPattern({ + name: _id, }) .then(() => itemsPaused.push(_id)) - .catch((err) => { - errors.push({ id: _id, error: formatError(err) }); + .catch((error) => { + errors.push({ id: _id, error: handleEsError({ error, response }) }); }) ) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts index 904642d292e3c6e..1837f1c17b384c7 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense, mockError } from '../test_lib'; import { registerResumeRoute } from './register_resume_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => { registerResumeRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,7 +33,9 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => { it('resumes a single item', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + ccr: { + resumeAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -51,11 +49,13 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => { it('resumes multiple items', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + resumeAutoFollowPattern: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -69,10 +69,12 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => { it('returns partial errors', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockRejectedValueOnce({ response: { error: {} } }), + ccr: { + resumeAutoFollowPattern: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce(mockError), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts index 1c6396d0b350223..912ecd689054d1d 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerResumeRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string(), @@ -29,29 +29,22 @@ export const registerResumeRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const ids = id.split(','); const itemsResumed: string[] = []; const errors: Array<{ id: string; error: any }> = []; - const formatError = (err: any) => { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - return response.customError({ statusCode: 500, body: err }); - }; - await Promise.all( ids.map((_id: string) => - context - .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', { - id: _id, + client.asCurrentUser.ccr + .resumeAutoFollowPattern({ + name: _id, }) .then(() => itemsResumed.push(_id)) - .catch((err) => { - errors.push({ id: _id, error: formatError(err) }); + .catch((error) => { + errors.push({ id: _id, error: handleEsError({ error, response }) }); }) ) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts index 66f4ddb950ef084..5ada82049632ba4 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense } from '../test_lib'; import { registerUpdateRoute } from './register_update_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Update auto-follow pattern', () => { registerUpdateRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,8 +33,10 @@ describe('[CCR API] Update auto-follow pattern', () => { it('should serialize the payload before sending it to Elasticsearch', async () => { const routeContextMock = mockRouteContext({ - // Just echo back what we send so we can inspect it. - callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload), + ccr: { + // Just echo back what we send so we can inspect it. + putAutoFollowPattern: jest.fn().mockImplementation((payload) => ({ body: payload })), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -53,7 +51,7 @@ describe('[CCR API] Update auto-follow pattern', () => { const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); expect(response.payload).toEqual({ - id: 'foo', + name: 'foo', body: { remote_cluster: 'bar1', leader_index_patterns: ['bar2'], diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts index a3e7c3544ca3706..81aaa59725e3a09 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types'; export const registerUpdateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string(), @@ -39,22 +39,21 @@ export const registerUpdateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const body = serializeAutoFollowPattern(request.body as AutoFollowPattern); try { + const { body: responseBody } = await client.asCurrentUser.ccr.putAutoFollowPattern({ + name: id, + body, + }); + return response.ok({ - body: await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.saveAutoFollowPattern', - { id, body } - ), + body: responseBody, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts index 130adb2e0b98925..600924e14503998 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -14,7 +14,7 @@ import { RouteDependencies } from '../../../types'; export const registerPermissionsRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -22,6 +22,8 @@ export const registerPermissionsRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; + if (!license.isEsSecurityEnabled) { // If security has been disabled in elasticsearch.yml. we'll just let the user use CCR // because permissions are irrelevant. @@ -35,9 +37,8 @@ export const registerPermissionsRoute = ({ try { const { - has_all_requested: hasPermission, - cluster, - } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', { + body: { has_all_requested: hasPermission, cluster }, + } = await client.asCurrentUser.security.hasPrivileges({ body: { cluster: ['manage', 'manage_ccr'], }, @@ -59,12 +60,8 @@ export const registerPermissionsRoute = ({ missingClusterPrivileges, }, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts index 6636f6b1c5accc0..fa8f792afc4197b 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerStatsRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -23,20 +23,19 @@ export const registerStatsRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; + try { const { - auto_follow_stats: autoFollowStats, - } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + body: { auto_follow_stats: autoFollowStats }, + } = await client.asCurrentUser.ccr.stats(); return response.ok({ + // @ts-expect-error Once #98266 is merged, test this again. body: deserializeAutoFollowStats(autoFollowStats), }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts index 44cb62776856d0d..8de168e8a17d1c3 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense } from '../test_lib'; import { registerCreateRoute } from './register_create_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Create follower index', () => { registerCreateRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,7 +33,9 @@ describe('[CCR API] Create follower index', () => { it('should return 200 status when follower index is created', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + ccr: { + follow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts index f44e5a749baad79..9ddbf76cceed289 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -18,7 +18,7 @@ import { RouteDependencies } from '../../../types'; export const registerCreateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const bodySchema = schema.object({ name: schema.string(), @@ -44,22 +44,21 @@ export const registerCreateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { name, ...rest } = request.body; const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex)); try { + const { body: responseBody } = await client.asCurrentUser.ccr.follow({ + index: name, + body, + }); + return response.ok({ - body: await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.saveFollowerIndex', - { name, body } - ), + body: responseBody, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts index b668cea06f90f0b..e4063e1e64df529 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense } from '../test_lib'; import { registerFetchRoute } from './register_fetch_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Fetch all follower indices', () => { registerFetchRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,73 +33,77 @@ describe('[CCR API] Fetch all follower indices', () => { it('deserializes the response from Elasticsearch', async () => { const ccrInfoMockResponse = { - follower_indices: [ - { - follower_index: 'followerIndexName', - remote_cluster: 'remoteCluster', - leader_index: 'leaderIndex', - status: 'active', - parameters: { - max_read_request_operation_count: 1, - max_outstanding_read_requests: 1, - max_read_request_size: '1b', - max_write_request_operation_count: 1, - max_write_request_size: '1b', - max_outstanding_write_requests: 1, - max_write_buffer_count: 1, - max_write_buffer_size: '1b', - max_retry_delay: '1s', - read_poll_timeout: '1s', + body: { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, }, - }, - ], + ], + }, }; // These stats correlate to the above follower indices. const ccrStatsMockResponse = { - follow_stats: { - indices: [ - { - index: 'followerIndexName', - shards: [ - { - shard_id: 1, - leader_index: 'leaderIndex', - leader_global_checkpoint: 1, - leader_max_seq_no: 1, - follower_global_checkpoint: 1, - follower_max_seq_no: 1, - last_requested_seq_no: 1, - outstanding_read_requests: 1, - outstanding_write_requests: 1, - write_buffer_operation_count: 1, - write_buffer_size_in_bytes: 1, - follower_mapping_version: 1, - follower_settings_version: 1, - total_read_time_millis: 1, - total_read_remote_exec_time_millis: 1, - successful_read_requests: 1, - failed_read_requests: 1, - operations_read: 1, - bytes_read: 1, - total_write_time_millis: 1, - successful_write_requests: 1, - failed_write_requests: 1, - operations_written: 1, - read_exceptions: 1, - time_since_last_read_millis: 1, - }, - ], - }, - ], + body: { + follow_stats: { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }, }, }; const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce(ccrInfoMockResponse) - .mockResolvedValueOnce(ccrStatsMockResponse), + ccr: { + followInfo: jest.fn().mockResolvedValueOnce(ccrInfoMockResponse), + stats: jest.fn().mockResolvedValueOnce(ccrStatsMockResponse), + }, }); const request = httpServerMock.createKibanaRequest(); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts index c72706cf5d10dc1..b4d7ead75e8302d 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerFetchRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -23,16 +23,18 @@ export const registerFetchRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; + try { const { - follower_indices: followerIndices, - } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { - id: '_all', - }); + body: { follower_indices: followerIndices }, + } = await client.asCurrentUser.ccr.followInfo({ index: '_all' }); const { - follow_stats: { indices: followerIndicesStats }, - } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + body: { + follow_stats: { indices: followerIndicesStats }, + }, + } = await client.asCurrentUser.ccr.stats(); const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { map[stats.index] = stats; @@ -51,12 +53,8 @@ export const registerFetchRoute = ({ indices: deserializeListFollowerIndices(collatedFollowerIndices), }, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts index f5d3ecdd09e8d33..493703ca6de2772 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense } from '../test_lib'; import { registerGetRoute } from './register_get_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Get one follower index', () => { registerGetRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,71 +33,75 @@ describe('[CCR API] Get one follower index', () => { it('should return a single resource even though ES returns an array with 1 item', async () => { const ccrInfoMockResponse = { - follower_indices: [ - { - follower_index: 'followerIndexName', - remote_cluster: 'remoteCluster', - leader_index: 'leaderIndex', - status: 'active', - parameters: { - max_read_request_operation_count: 1, - max_outstanding_read_requests: 1, - max_read_request_size: '1b', - max_write_request_operation_count: 1, - max_write_request_size: '1b', - max_outstanding_write_requests: 1, - max_write_buffer_count: 1, - max_write_buffer_size: '1b', - max_retry_delay: '1s', - read_poll_timeout: '1s', + body: { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, }, - }, - ], + ], + }, }; // These stats correlate to the above follower indices. const ccrFollowerIndexStatsMockResponse = { - indices: [ - { - index: 'followerIndexName', - shards: [ - { - shard_id: 1, - leader_index: 'leaderIndex', - leader_global_checkpoint: 1, - leader_max_seq_no: 1, - follower_global_checkpoint: 1, - follower_max_seq_no: 1, - last_requested_seq_no: 1, - outstanding_read_requests: 1, - outstanding_write_requests: 1, - write_buffer_operation_count: 1, - write_buffer_size_in_bytes: 1, - follower_mapping_version: 1, - follower_settings_version: 1, - total_read_time_millis: 1, - total_read_remote_exec_time_millis: 1, - successful_read_requests: 1, - failed_read_requests: 1, - operations_read: 1, - bytes_read: 1, - total_write_time_millis: 1, - successful_write_requests: 1, - failed_write_requests: 1, - operations_written: 1, - read_exceptions: 1, - time_since_last_read_millis: 1, - }, - ], - }, - ], + body: { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }, }; const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce(ccrInfoMockResponse) - .mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse), + ccr: { + followInfo: jest.fn().mockResolvedValueOnce(ccrInfoMockResponse), + followStats: jest.fn().mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts index cdcfff97e645db9..21cb9e08a616051 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -16,7 +16,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string(), @@ -30,12 +30,13 @@ export const registerGetRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; try { const { - follower_indices: followerIndices, - } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + body: { follower_indices: followerIndices }, + } = await client.asCurrentUser.ccr.followInfo({ index: id }); const followerIndexInfo = followerIndices && followerIndices[0]; @@ -48,31 +49,26 @@ export const registerGetRoute = ({ // If this follower is paused, skip call to ES stats api since it will return 404 if (followerIndexInfo.status === 'paused') { return response.ok({ + // @ts-expect-error Once #98266 is merged, test this again. body: deserializeFollowerIndex({ ...followerIndexInfo, }), }); } else { const { - indices: followerIndicesStats, - } = await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.followerIndexStats', - { id } - ); + body: { indices: followerIndicesStats }, + } = await client.asCurrentUser.ccr.followStats({ index: id }); return response.ok({ + // @ts-expect-error Once #98266 is merged, test this again. body: deserializeFollowerIndex({ ...followerIndexInfo, ...(followerIndicesStats ? followerIndicesStats[0] : {}), }), }); } - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts index 2c6854390ba5f50..920de2777ad2e0f 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense, mockError } from '../test_lib'; import { registerPauseRoute } from './register_pause_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Pause follower index/indices', () => { registerPauseRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,7 +33,9 @@ describe('[CCR API] Pause follower index/indices', () => { it('pauses a single item', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + ccr: { + pauseFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -51,11 +49,13 @@ describe('[CCR API] Pause follower index/indices', () => { it('pauses multiple items', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + pauseFollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -69,10 +69,12 @@ describe('[CCR API] Pause follower index/indices', () => { it('returns partial errors', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockRejectedValueOnce({ response: { error: {} } }), + ccr: { + pauseFollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce(mockError), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts index 2e4e71278df0e99..6c888e0a6e8bd06 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerPauseRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string() }); @@ -27,29 +27,20 @@ export const registerPauseRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const ids = id.split(','); const itemsPaused: string[] = []; const errors: Array<{ id: string; error: any }> = []; - const formatError = (err: any) => { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - return response.customError({ statusCode: 500, body: err }); - }; - await Promise.all( ids.map((_id: string) => - context - .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', { - id: _id, - }) + client.asCurrentUser.ccr + .pauseFollow({ index: _id }) .then(() => itemsPaused.push(_id)) - .catch((err) => { - errors.push({ id: _id, error: formatError(err) }); + .catch((error) => { + errors.push({ id: _id, error: handleEsError({ error, response }) }); }) ) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts index 3040bc822bf2fb0..1cd54a898dc397d 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense, mockError } from '../test_lib'; import { registerResumeRoute } from './register_resume_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Resume follower index/indices', () => { registerResumeRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,7 +33,9 @@ describe('[CCR API] Resume follower index/indices', () => { it('resumes a single item', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + ccr: { + resumeFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -51,11 +49,13 @@ describe('[CCR API] Resume follower index/indices', () => { it('resumes multiple items', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + resumeFollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -69,10 +69,12 @@ describe('[CCR API] Resume follower index/indices', () => { it('returns partial errors', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockRejectedValueOnce({ response: { error: {} } }), + ccr: { + resumeFollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce(mockError), + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts index 34f204f3b64b96e..206de7d62fb3842 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerResumeRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string() }); @@ -27,29 +27,20 @@ export const registerResumeRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const ids = id.split(','); const itemsResumed: string[] = []; const errors: Array<{ id: string; error: any }> = []; - const formatError = (err: any) => { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - return response.customError({ statusCode: 500, body: err }); - }; - await Promise.all( ids.map((_id: string) => - context - .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', { - id: _id, - }) + client.asCurrentUser.ccr + .resumeFollow({ index: _id }) .then(() => itemsResumed.push(_id)) - .catch((err) => { - errors.push({ id: _id, error: formatError(err) }); + .catch((error) => { + errors.push({ id: _id, error: handleEsError({ error, response }) }); }) ) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts index b4dfca304651236..8ef9c84594a461f 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts @@ -8,9 +8,8 @@ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { isEsError, License } from '../../../shared_imports'; -import { formatEsError } from '../../../lib/format_es_error'; -import { mockRouteContext } from '../test_lib'; +import { handleEsError } from '../../../shared_imports'; +import { mockRouteContext, mockLicense, mockError } from '../test_lib'; import { registerUnfollowRoute } from './register_unfollow_route'; const httpService = httpServiceMock.createSetupContract(); @@ -23,12 +22,9 @@ describe('[CCR API] Unfollow follower index/indices', () => { registerUnfollowRoute({ router, - license: { - guardApiRoute: (route: any) => route, - } as License, + license: mockLicense, lib: { - isEsError, - formatEsError, + handleEsError, }, }); @@ -37,12 +33,14 @@ describe('[CCR API] Unfollow follower index/indices', () => { it('unfollows a single item', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + pauseFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + unfollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, + indices: { + close: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + open: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }, }); const request = httpServerMock.createKibanaRequest({ @@ -56,23 +54,30 @@ describe('[CCR API] Unfollow follower index/indices', () => { it('unfollows multiple items', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - // a - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - // b - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - // c - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }), + ccr: { + pauseFollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) // a + .mockResolvedValueOnce({ acknowledge: true }) // b + .mockResolvedValueOnce({ acknowledge: true }), // c + unfollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) // a + .mockResolvedValueOnce({ acknowledge: true }) // b + .mockResolvedValueOnce({ acknowledge: true }), // c + }, + indices: { + close: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) // a + .mockResolvedValueOnce({ acknowledge: true }) // b + .mockResolvedValueOnce({ acknowledge: true }), // c + open: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) // a + .mockResolvedValueOnce({ acknowledge: true }) // b + .mockResolvedValueOnce({ acknowledge: true }), // c + }, }); const request = httpServerMock.createKibanaRequest({ @@ -86,16 +91,20 @@ describe('[CCR API] Unfollow follower index/indices', () => { it('returns partial errors', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest - .fn() - // a - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - .mockResolvedValueOnce({ acknowledge: true }) - // b - .mockResolvedValueOnce({ acknowledge: true }) - .mockRejectedValueOnce({ response: { error: {} } }), + ccr: { + pauseFollow: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) // a + .mockResolvedValueOnce({ acknowledge: true }), // B + unfollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), // a + }, + indices: { + close: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) // a + .mockRejectedValueOnce(mockError), // b + open: jest.fn().mockResolvedValueOnce({ acknowledge: true }), // a + }, }); const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts index 848408e14662fdd..e240a10df068430 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types'; export const registerUnfollowRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string() }); @@ -27,6 +27,7 @@ export const registerUnfollowRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; const ids = id.split(','); @@ -34,52 +35,34 @@ export const registerUnfollowRoute = ({ const itemsNotOpen: string[] = []; const errors: Array<{ id: string; error: any }> = []; - const formatError = (err: any) => { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - return response.customError({ statusCode: 500, body: err }); - }; - await Promise.all( ids.map(async (_id: string) => { try { // Try to pause follower, let it fail silently since it may already be paused try { - await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.pauseFollowerIndex', - { id: _id } - ); + await client.asCurrentUser.ccr.pauseFollow({ index: _id }); } catch (e) { // Swallow errors } // Close index - await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', { - index: _id, - }); + await client.asCurrentUser.indices.close({ index: _id }); // Unfollow leader - await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.unfollowLeaderIndex', - { id: _id } - ); + await client.asCurrentUser.ccr.unfollow({ index: _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 context.crossClusterReplication!.client.callAsCurrentUser('indices.open', { - index: _id, - }); + await client.asCurrentUser.indices.open({ index: _id }); } catch (e) { itemsNotOpen.push(_id); } // Push success itemsUnfollowed.push(_id); - } catch (err) { - errors.push({ id: _id, error: formatError(err) }); + } catch (error) { + errors.push({ id: _id, error: handleEsError({ error, response }) }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts index 933d13a0a223c02..5029c8be96043ae 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -18,7 +18,7 @@ import { RouteDependencies } from '../../../types'; export const registerUpdateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { const paramsSchema = schema.object({ id: schema.string() }); @@ -44,13 +44,14 @@ export const registerUpdateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client } = context.core.elasticsearch; const { id } = request.params; // We need to first pause the follower and then resume it by passing the advanced settings try { const { - follower_indices: followerIndices, - } = await context.crossClusterReplication.client.callAsCurrentUser('ccr.info', { id }); + body: { follower_indices: followerIndices }, + } = await client.asCurrentUser.ccr.followInfo({ index: id }); const followerIndexInfo = followerIndices && followerIndices[0]; @@ -63,12 +64,7 @@ export const registerUpdateRoute = ({ // Pause follower if not already paused if (!isPaused) { - await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.pauseFollowerIndex', - { - id, - } - ); + await client.asCurrentUser.ccr.pauseFollow({ index: id }); } // Resume follower @@ -76,18 +72,16 @@ export const registerUpdateRoute = ({ serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings) ); + const { body: responseBody } = await client.asCurrentUser.ccr.resumeFollow({ + index: id, + body, + }); + return response.ok({ - body: await context.crossClusterReplication!.client.callAsCurrentUser( - 'ccr.resumeFollowerIndex', - { id, body } - ), + body: responseBody, }); - } catch (err) { - if (isEsError(err)) { - return response.customError(formatEsError(err)); - } - // Case: default - throw err; + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts index e2aa8a478e91638..3a5a317797933e4 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts @@ -6,19 +6,24 @@ */ import { RequestHandlerContext } from 'src/core/server'; +import { License } from '../../shared_imports'; -export function mockRouteContext({ - callAsCurrentUser, -}: { - callAsCurrentUser: any; -}): RequestHandlerContext { +export function mockRouteContext(mockedFunctions: unknown): RequestHandlerContext { const routeContextMock = ({ - crossClusterReplication: { - client: { - callAsCurrentUser, + core: { + elasticsearch: { + client: { + asCurrentUser: mockedFunctions, + }, }, }, } as unknown) as RequestHandlerContext; return routeContextMock; } + +export const mockLicense = { + guardApiRoute: (route: any) => route, +} as License; + +export const mockError = { name: 'ResponseError', statusCode: 400 }; diff --git a/x-pack/plugins/cross_cluster_replication/server/shared_imports.ts b/x-pack/plugins/cross_cluster_replication/server/shared_imports.ts index 4252a2a5c32d4dd..e9e3ed72aed6445 100644 --- a/x-pack/plugins/cross_cluster_replication/server/shared_imports.ts +++ b/x-pack/plugins/cross_cluster_replication/server/shared_imports.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; export { License } from '../../license_api_guard/server'; diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts index 7314fda70027f24..8d7d86c735859a9 100644 --- a/x-pack/plugins/cross_cluster_replication/server/types.ts +++ b/x-pack/plugins/cross_cluster_replication/server/types.ts @@ -5,13 +5,12 @@ * 2.0. */ -import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; +import { IRouter, RequestHandlerContext } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; -import { License, isEsError } from './shared_imports'; -import { formatEsError } from './lib/format_es_error'; +import { License, handleEsError } from './shared_imports'; export interface SetupDependencies { licensing: LicensingPluginSetup; @@ -25,24 +24,9 @@ export interface StartDependencies { } export interface RouteDependencies { - router: CcrPluginRouter; + router: IRouter; license: License; lib: { - isEsError: typeof isEsError; - formatEsError: typeof formatEsError; + handleEsError: typeof handleEsError; }; } - -/** - * @internal - */ -export interface CcrRequestHandlerContext extends RequestHandlerContext { - crossClusterReplication: { - client: ILegacyScopedClusterClient; - }; -} - -/** - * @internal - */ -type CcrPluginRouter = IRouter;