Migrate CCR to new ES JS client. (#100131) (#101125)

* Update SectionError component to render error root causes correctly.
* Fix 404 error rendering.
* Add test for follower index update API route.
This commit is contained in:
CJ Cenizal 2021-06-01 18:24:18 -07:00 committed by GitHub
parent d6cb25aeea
commit e044137bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 652 additions and 977 deletions

View file

@ -4,10 +4,11 @@
You can run a local cluster and simulate a remote cluster within a single Kibana directory. 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. 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. Start your "remote" cluster by running `yarn es snapshot --license=trial -E cluster.name=europe -E transport.port=9400` in a separate terminal tab. 2. Create a "remote" copy of your ES snapshot by running: `cp -R .es/8.0.0 .es/8.0.0-2`.
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"`. 4. Start your "remote" cluster by running `.es/8.0.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400`.
Note that these settings are required for testing auto-follow pattern conflicts errors (see below). 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` 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`. index on the remote cluster that's available at `127.0.0.1:9400`.

View file

@ -11,21 +11,19 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
export function SectionError(props) { export function SectionError(props) {
const { title, error, ...rest } = props; const { title, error, ...rest } = props;
const data = error.body ? error.body : error; const data = error.body ? error.body : error;
const { const { error: errorString, attributes, message } = data;
error: errorString,
attributes, // wrapEsError() on the server add a "cause" array
message,
} = data;
return ( return (
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}> <EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
<div>{message || errorString}</div> <div>{message || errorString}</div>
{attributes && attributes.cause && ( {attributes?.error?.root_cause && (
<Fragment> <Fragment>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<ul> <ul>
{attributes.cause.map((message, i) => ( {attributes.error.root_cause.map(({ type, reason }, i) => (
<li key={i}>{message}</li> <li key={i}>
{type}: {reason}
</li>
))} ))}
</ul> </ul>
</Fragment> </Fragment>

View file

@ -79,24 +79,24 @@ export class AutoFollowPatternEdit extends PureComponent {
params: { id: name }, params: { id: name },
}, },
} = this.props; } = this.props;
const title = i18n.translate( const title = i18n.translate(
'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle', 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle',
{ {
defaultMessage: 'Error loading auto-follow pattern', defaultMessage: 'Error loading auto-follow pattern',
} }
); );
const errorMessage = const errorMessage =
error.status === 404 error.body.statusCode === 404
? { ? {
data: { error: i18n.translate(
error: i18n.translate( 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorMessage',
'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorMessage', {
{ defaultMessage: `The auto-follow pattern '{name}' does not exist.`,
defaultMessage: `The auto-follow pattern '{name}' does not exist.`, values: { name },
values: { name }, }
} ),
),
},
} }
: error; : error;

View file

@ -121,24 +121,24 @@ export class FollowerIndexEdit extends PureComponent {
params: { id: name }, params: { id: name },
}, },
} = this.props; } = this.props;
const title = i18n.translate( const title = i18n.translate(
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle', 'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle',
{ {
defaultMessage: 'Error loading follower index', defaultMessage: 'Error loading follower index',
} }
); );
const errorMessage = const errorMessage =
error.status === 404 error.body.statusCode === 404
? { ? {
data: { error: i18n.translate(
error: i18n.translate( 'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage', {
{ defaultMessage: `The follower index '{name}' does not exist.`,
defaultMessage: `The follower index '{name}' does not exist.`, values: { name },
values: { name }, }
} ),
),
},
} }
: error; : error;

View file

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

View file

@ -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<string, any> = {},
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<string, string> = {}
): { 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,
},
},
};
}

View file

@ -10,7 +10,6 @@ import { first } from 'rxjs/operators';
import { import {
CoreSetup, CoreSetup,
CoreStart, CoreStart,
ILegacyCustomClusterClient,
Plugin, Plugin,
Logger, Logger,
PluginInitializerContext, PluginInitializerContext,
@ -19,20 +18,12 @@ import {
import { Index } from '../../index_management/server'; import { Index } from '../../index_management/server';
import { PLUGIN } from '../common/constants'; import { PLUGIN } from '../common/constants';
import { SetupDependencies, StartDependencies, CcrRequestHandlerContext } from './types'; import { SetupDependencies, StartDependencies } from './types';
import { registerApiRoutes } from './routes'; import { registerApiRoutes } from './routes';
import { elasticsearchJsPlugin } from './client/elasticsearch_ccr';
import { CrossClusterReplicationConfig } from './config'; import { CrossClusterReplicationConfig } from './config';
import { License, isEsError } from './shared_imports'; import { License, handleEsError } 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);
}
// TODO replace deprecated ES client after Index Management is updated
const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => { const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => {
if (!indicesList?.length) { if (!indicesList?.length) {
return indicesList; return indicesList;
@ -66,7 +57,6 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
private readonly config$: Observable<CrossClusterReplicationConfig>; private readonly config$: Observable<CrossClusterReplicationConfig>;
private readonly license: License; private readonly license: License;
private readonly logger: Logger; private readonly logger: Logger;
private ccrEsClient?: ILegacyCustomClusterClient;
constructor(initializerContext: PluginInitializerContext) { constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get(); this.logger = initializerContext.logger.get();
@ -114,22 +104,11 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
], ],
}); });
http.registerRouteHandlerContext<CcrRequestHandlerContext, 'crossClusterReplication'>(
'crossClusterReplication',
async (ctx, request) => {
this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices));
return {
client: this.ccrEsClient.asScoped(request),
};
}
);
registerApiRoutes({ registerApiRoutes({
router: http.createRouter(), router: http.createRouter(),
license: this.license, license: this.license,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
} }
@ -142,9 +121,5 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
}); });
} }
stop() { stop() {}
if (this.ccrEsClient) {
this.ccrEsClient.close();
}
}
} }

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerCreateRoute } from './register_create_route'; import { registerCreateRoute } from './register_create_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Create auto-follow pattern', () => {
registerCreateRoute({ registerCreateRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,8 +33,10 @@ describe('[CCR API] Create auto-follow pattern', () => {
it('should throw a 409 conflict error if id already exists', async () => { it('should throw a 409 conflict error if id already exists', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
// Fail the uniqueness check. ccr: {
callAsCurrentUser: jest.fn().mockResolvedValueOnce(true), // Fail the uniqueness check.
getAutoFollowPattern: jest.fn().mockResolvedValueOnce(true),
},
}); });
const request = httpServerMock.createKibanaRequest({ 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 () => { it('should return 200 status when the id does not exist', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn()
// Pass the uniqueness check. // Pass the uniqueness check.
.mockRejectedValueOnce({ statusCode: 404 }) getAutoFollowPattern: jest.fn().mockRejectedValueOnce({ statusCode: 404 }),
.mockResolvedValueOnce(true), putAutoFollowPattern: jest.fn().mockResolvedValueOnce(true),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types';
export const registerCreateRoute = ({ export const registerCreateRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const bodySchema = schema.object({ const bodySchema = schema.object({
id: schema.string(), id: schema.string(),
@ -34,6 +34,7 @@ export const registerCreateRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id, ...rest } = request.body; const { id, ...rest } = request.body;
const body = serializeAutoFollowPattern(rest as AutoFollowPattern); const body = serializeAutoFollowPattern(rest as AutoFollowPattern);
@ -42,36 +43,29 @@ export const registerCreateRoute = ({
* the same id does not exist. * the same id does not exist.
*/ */
try { try {
await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', { await client.asCurrentUser.ccr.getAutoFollowPattern({ name: id });
id,
});
// If we get here it means that an auto-follow pattern with the same id exists // If we get here it means that an auto-follow pattern with the same id exists
return response.conflict({ return response.conflict({
body: `An auto-follow pattern with the name "${id}" already exists.`, body: `An auto-follow pattern with the name "${id}" already exists.`,
}); });
} catch (err) { } catch (error) {
if (err.statusCode !== 404) { if (error.statusCode !== 404) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
} }
try { try {
return response.ok({ const { body: responseBody } = await client.asCurrentUser.ccr.putAutoFollowPattern({
body: await context.crossClusterReplication!.client.callAsCurrentUser( name: id,
'ccr.saveAutoFollowPattern', body,
{ id, body }
),
}); });
} catch (err) {
if (isEsError(err)) { return response.ok({
return response.customError(formatEsError(err)); body: responseBody,
} });
// Case: default } catch (error) {
throw err; return handleEsError({ error, response });
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense, mockError } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerDeleteRoute } from './register_delete_route'; import { registerDeleteRoute } from './register_delete_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
registerDeleteRoute({ registerDeleteRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,7 +33,9 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
it('deletes a single item', async () => { it('deletes a single item', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), ccr: {
deleteAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -51,11 +49,13 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
it('deletes multiple items', async () => { it('deletes multiple items', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() deleteAutoFollowPattern: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }), .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -69,10 +69,12 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
it('returns partial errors', async () => { it('returns partial errors', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() deleteAutoFollowPattern: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockRejectedValueOnce({ response: { error: {} } }), .mockResolvedValueOnce({ acknowledge: true })
.mockRejectedValueOnce(mockError),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -16,7 +16,7 @@ import { RouteDependencies } from '../../../types';
export const registerDeleteRoute = ({ export const registerDeleteRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ const paramsSchema = schema.object({
id: schema.string(), id: schema.string(),
@ -30,29 +30,22 @@ export const registerDeleteRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const ids = id.split(','); const ids = id.split(',');
const itemsDeleted: string[] = []; const itemsDeleted: string[] = [];
const errors: Array<{ id: string; error: any }> = []; 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( await Promise.all(
ids.map((_id) => ids.map((_id) =>
context client.asCurrentUser.ccr
.crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', { .deleteAutoFollowPattern({
id: _id, name: _id,
}) })
.then(() => itemsDeleted.push(_id)) .then(() => itemsDeleted.push(_id))
.catch((err: any) => { .catch((error: any) => {
errors.push({ id: _id, error: formatError(err) }); errors.push({ id: _id, error: handleEsError({ error, response }) });
}) })
) )
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerFetchRoute } from './register_fetch_route'; import { registerFetchRoute } from './register_fetch_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Fetch all auto-follow patterns', () => {
registerFetchRoute({ registerFetchRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,21 +33,25 @@ describe('[CCR API] Fetch all auto-follow patterns', () => {
it('deserializes the response from Elasticsearch', async () => { it('deserializes the response from Elasticsearch', async () => {
const ccrAutoFollowPatternResponseMock = { const ccrAutoFollowPatternResponseMock = {
patterns: [ body: {
{ patterns: [
name: 'autoFollowPattern', {
pattern: { name: 'autoFollowPattern',
active: true, pattern: {
remote_cluster: 'remoteCluster', active: true,
leader_index_patterns: ['leader*'], remote_cluster: 'remoteCluster',
follow_index_pattern: 'follow', leader_index_patterns: ['leader*'],
follow_index_pattern: 'follow',
},
}, },
}, ],
], },
}; };
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), ccr: {
getAutoFollowPattern: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
},
}); });
const request = httpServerMock.createKibanaRequest(); const request = httpServerMock.createKibanaRequest();

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerFetchRoute = ({ export const registerFetchRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
router.get( router.get(
{ {
@ -23,21 +23,20 @@ export const registerFetchRoute = ({
validate: false, validate: false,
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
try { try {
const result = await context.crossClusterReplication!.client.callAsCurrentUser( const {
'ccr.autoFollowPatterns' body: { patterns },
); } = await client.asCurrentUser.ccr.getAutoFollowPattern();
return response.ok({ return response.ok({
body: { body: {
patterns: deserializeListAutoFollowPatterns(result.patterns), // @ts-expect-error Once #98266 is merged, test this again.
patterns: deserializeListAutoFollowPatterns(patterns),
}, },
}); });
} catch (err) { } catch (error) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerGetRoute } from './register_get_route'; import { registerGetRoute } from './register_get_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Get one auto-follow pattern', () => {
registerGetRoute({ registerGetRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -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 () => { it('should return a single resource even though ES returns an array with 1 item', async () => {
const ccrAutoFollowPatternResponseMock = { const ccrAutoFollowPatternResponseMock = {
patterns: [ body: {
{ patterns: [
name: 'autoFollowPattern', {
pattern: { name: 'autoFollowPattern',
active: true, pattern: {
remote_cluster: 'remoteCluster', active: true,
leader_index_patterns: ['leader*'], remote_cluster: 'remoteCluster',
follow_index_pattern: 'follow', leader_index_patterns: ['leader*'],
follow_index_pattern: 'follow',
},
}, },
}, ],
], },
}; };
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), ccr: {
getAutoFollowPattern: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
},
}); });
const request = httpServerMock.createKibanaRequest(); const request = httpServerMock.createKibanaRequest();

View file

@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types';
export const registerGetRoute = ({ export const registerGetRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ const paramsSchema = schema.object({
id: schema.string(), id: schema.string(),
@ -31,24 +31,22 @@ export const registerGetRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
try { try {
const result = await context.crossClusterReplication!.client.callAsCurrentUser( const result = await client.asCurrentUser.ccr.getAutoFollowPattern({
'ccr.autoFollowPattern', name: id,
{ id } });
);
const autoFollowPattern = result.patterns[0]; const autoFollowPattern = result.body.patterns[0];
return response.ok({ return response.ok({
// @ts-expect-error Once #98266 is merged, test this again.
body: deserializeAutoFollowPattern(autoFollowPattern), body: deserializeAutoFollowPattern(autoFollowPattern),
}); });
} catch (err) { } catch (error) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense, mockError } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerPauseRoute } from './register_pause_route'; import { registerPauseRoute } from './register_pause_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
registerPauseRoute({ registerPauseRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,7 +33,9 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
it('pauses a single item', async () => { it('pauses a single item', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), ccr: {
pauseAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -51,11 +49,13 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
it('pauses multiple items', async () => { it('pauses multiple items', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseAutoFollowPattern: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }), .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -69,10 +69,12 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
it('returns partial errors', async () => { it('returns partial errors', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseAutoFollowPattern: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockRejectedValueOnce({ response: { error: {} } }), .mockResolvedValueOnce({ acknowledge: true })
.mockRejectedValueOnce(mockError),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerPauseRoute = ({ export const registerPauseRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ const paramsSchema = schema.object({
id: schema.string(), id: schema.string(),
@ -29,29 +29,22 @@ export const registerPauseRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const ids = id.split(','); const ids = id.split(',');
const itemsPaused: string[] = []; const itemsPaused: string[] = [];
const errors: Array<{ id: string; error: any }> = []; 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( await Promise.all(
ids.map((_id) => ids.map((_id) =>
context client.asCurrentUser.ccr
.crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', { .pauseAutoFollowPattern({
id: _id, name: _id,
}) })
.then(() => itemsPaused.push(_id)) .then(() => itemsPaused.push(_id))
.catch((err) => { .catch((error) => {
errors.push({ id: _id, error: formatError(err) }); errors.push({ id: _id, error: handleEsError({ error, response }) });
}) })
) )
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense, mockError } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerResumeRoute } from './register_resume_route'; import { registerResumeRoute } from './register_resume_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
registerResumeRoute({ registerResumeRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,7 +33,9 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
it('resumes a single item', async () => { it('resumes a single item', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), ccr: {
resumeAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -51,11 +49,13 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
it('resumes multiple items', async () => { it('resumes multiple items', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() resumeAutoFollowPattern: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }), .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -69,10 +69,12 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
it('returns partial errors', async () => { it('returns partial errors', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() resumeAutoFollowPattern: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockRejectedValueOnce({ response: { error: {} } }), .mockResolvedValueOnce({ acknowledge: true })
.mockRejectedValueOnce(mockError),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerResumeRoute = ({ export const registerResumeRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ const paramsSchema = schema.object({
id: schema.string(), id: schema.string(),
@ -29,29 +29,22 @@ export const registerResumeRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const ids = id.split(','); const ids = id.split(',');
const itemsResumed: string[] = []; const itemsResumed: string[] = [];
const errors: Array<{ id: string; error: any }> = []; 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( await Promise.all(
ids.map((_id: string) => ids.map((_id: string) =>
context client.asCurrentUser.ccr
.crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', { .resumeAutoFollowPattern({
id: _id, name: _id,
}) })
.then(() => itemsResumed.push(_id)) .then(() => itemsResumed.push(_id))
.catch((err) => { .catch((error) => {
errors.push({ id: _id, error: formatError(err) }); errors.push({ id: _id, error: handleEsError({ error, response }) });
}) })
) )
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerUpdateRoute } from './register_update_route'; import { registerUpdateRoute } from './register_update_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Update auto-follow pattern', () => {
registerUpdateRoute({ registerUpdateRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,8 +33,10 @@ describe('[CCR API] Update auto-follow pattern', () => {
it('should serialize the payload before sending it to Elasticsearch', async () => { it('should serialize the payload before sending it to Elasticsearch', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
// Just echo back what we send so we can inspect it. ccr: {
callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload), // Just echo back what we send so we can inspect it.
putAutoFollowPattern: jest.fn().mockImplementation((payload) => ({ body: payload })),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -53,7 +51,7 @@ describe('[CCR API] Update auto-follow pattern', () => {
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
expect(response.payload).toEqual({ expect(response.payload).toEqual({
id: 'foo', name: 'foo',
body: { body: {
remote_cluster: 'bar1', remote_cluster: 'bar1',
leader_index_patterns: ['bar2'], leader_index_patterns: ['bar2'],

View file

@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types';
export const registerUpdateRoute = ({ export const registerUpdateRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ const paramsSchema = schema.object({
id: schema.string(), id: schema.string(),
@ -39,22 +39,21 @@ export const registerUpdateRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const body = serializeAutoFollowPattern(request.body as AutoFollowPattern); const body = serializeAutoFollowPattern(request.body as AutoFollowPattern);
try { try {
return response.ok({ const { body: responseBody } = await client.asCurrentUser.ccr.putAutoFollowPattern({
body: await context.crossClusterReplication!.client.callAsCurrentUser( name: id,
'ccr.saveAutoFollowPattern', body,
{ id, body }
),
}); });
} catch (err) {
if (isEsError(err)) { return response.ok({
return response.customError(formatEsError(err)); body: responseBody,
} });
// Case: default } catch (error) {
throw err; return handleEsError({ error, response });
} }
}) })
); );

View file

@ -14,7 +14,7 @@ import { RouteDependencies } from '../../../types';
export const registerPermissionsRoute = ({ export const registerPermissionsRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
router.get( router.get(
{ {
@ -22,6 +22,8 @@ export const registerPermissionsRoute = ({
validate: false, validate: false,
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
if (!license.isEsSecurityEnabled) { if (!license.isEsSecurityEnabled) {
// If security has been disabled in elasticsearch.yml. we'll just let the user use CCR // If security has been disabled in elasticsearch.yml. we'll just let the user use CCR
// because permissions are irrelevant. // because permissions are irrelevant.
@ -35,9 +37,8 @@ export const registerPermissionsRoute = ({
try { try {
const { const {
has_all_requested: hasPermission, body: { has_all_requested: hasPermission, cluster },
cluster, } = await client.asCurrentUser.security.hasPrivileges({
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', {
body: { body: {
cluster: ['manage', 'manage_ccr'], cluster: ['manage', 'manage_ccr'],
}, },
@ -59,12 +60,8 @@ export const registerPermissionsRoute = ({
missingClusterPrivileges, missingClusterPrivileges,
}, },
}); });
} catch (err) { } catch (error) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
}) })
); );

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerStatsRoute = ({ export const registerStatsRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
router.get( router.get(
{ {
@ -23,20 +23,19 @@ export const registerStatsRoute = ({
validate: false, validate: false,
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
try { try {
const { const {
auto_follow_stats: autoFollowStats, body: { auto_follow_stats: autoFollowStats },
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); } = await client.asCurrentUser.ccr.stats();
return response.ok({ return response.ok({
// @ts-expect-error Once #98266 is merged, test this again.
body: deserializeAutoFollowStats(autoFollowStats), body: deserializeAutoFollowStats(autoFollowStats),
}); });
} catch (err) { } catch (error) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerCreateRoute } from './register_create_route'; import { registerCreateRoute } from './register_create_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Create follower index', () => {
registerCreateRoute({ registerCreateRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,7 +33,9 @@ describe('[CCR API] Create follower index', () => {
it('should return 200 status when follower index is created', async () => { it('should return 200 status when follower index is created', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), ccr: {
follow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -18,7 +18,7 @@ import { RouteDependencies } from '../../../types';
export const registerCreateRoute = ({ export const registerCreateRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const bodySchema = schema.object({ const bodySchema = schema.object({
name: schema.string(), name: schema.string(),
@ -44,22 +44,21 @@ export const registerCreateRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { name, ...rest } = request.body; const { name, ...rest } = request.body;
const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex)); const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex));
try { try {
return response.ok({ const { body: responseBody } = await client.asCurrentUser.ccr.follow({
body: await context.crossClusterReplication!.client.callAsCurrentUser( index: name,
'ccr.saveFollowerIndex', body,
{ name, body }
),
}); });
} catch (err) {
if (isEsError(err)) { return response.ok({
return response.customError(formatEsError(err)); body: responseBody,
} });
// Case: default } catch (error) {
throw err; return handleEsError({ error, response });
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerFetchRoute } from './register_fetch_route'; import { registerFetchRoute } from './register_fetch_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Fetch all follower indices', () => {
registerFetchRoute({ registerFetchRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,73 +33,77 @@ describe('[CCR API] Fetch all follower indices', () => {
it('deserializes the response from Elasticsearch', async () => { it('deserializes the response from Elasticsearch', async () => {
const ccrInfoMockResponse = { const ccrInfoMockResponse = {
follower_indices: [ 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', follower_index: 'followerIndexName',
shards: [ remote_cluster: 'remoteCluster',
{ leader_index: 'leaderIndex',
shard_id: 1, status: 'active',
leader_index: 'leaderIndex', parameters: {
leader_global_checkpoint: 1, max_read_request_operation_count: 1,
leader_max_seq_no: 1, max_outstanding_read_requests: 1,
follower_global_checkpoint: 1, max_read_request_size: '1b',
follower_max_seq_no: 1, max_write_request_operation_count: 1,
last_requested_seq_no: 1, max_write_request_size: '1b',
outstanding_read_requests: 1, max_outstanding_write_requests: 1,
outstanding_write_requests: 1, max_write_buffer_count: 1,
write_buffer_operation_count: 1, max_write_buffer_size: '1b',
write_buffer_size_in_bytes: 1, max_retry_delay: '1s',
follower_mapping_version: 1, read_poll_timeout: '1s',
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,
},
],
}, },
], ],
}, },
}; };
// These stats correlate to the above follower indices.
const ccrStatsMockResponse = {
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({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() followInfo: jest.fn().mockResolvedValueOnce(ccrInfoMockResponse),
.mockResolvedValueOnce(ccrInfoMockResponse) stats: jest.fn().mockResolvedValueOnce(ccrStatsMockResponse),
.mockResolvedValueOnce(ccrStatsMockResponse), },
}); });
const request = httpServerMock.createKibanaRequest(); const request = httpServerMock.createKibanaRequest();

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerFetchRoute = ({ export const registerFetchRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
router.get( router.get(
{ {
@ -23,16 +23,18 @@ export const registerFetchRoute = ({
validate: false, validate: false,
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
try { try {
const { const {
follower_indices: followerIndices, body: { follower_indices: followerIndices },
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { } = await client.asCurrentUser.ccr.followInfo({ index: '_all' });
id: '_all',
});
const { const {
follow_stats: { indices: followerIndicesStats }, body: {
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); follow_stats: { indices: followerIndicesStats },
},
} = await client.asCurrentUser.ccr.stats();
const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
map[stats.index] = stats; map[stats.index] = stats;
@ -51,12 +53,8 @@ export const registerFetchRoute = ({
indices: deserializeListFollowerIndices(collatedFollowerIndices), indices: deserializeListFollowerIndices(collatedFollowerIndices),
}, },
}); });
} catch (err) { } catch (error) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerGetRoute } from './register_get_route'; import { registerGetRoute } from './register_get_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Get one follower index', () => {
registerGetRoute({ registerGetRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -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 () => { it('should return a single resource even though ES returns an array with 1 item', async () => {
const ccrInfoMockResponse = { const ccrInfoMockResponse = {
follower_indices: [ body: {
{ follower_indices: [
follower_index: 'followerIndexName', {
remote_cluster: 'remoteCluster', follower_index: 'followerIndexName',
leader_index: 'leaderIndex', remote_cluster: 'remoteCluster',
status: 'active', leader_index: 'leaderIndex',
parameters: { status: 'active',
max_read_request_operation_count: 1, parameters: {
max_outstanding_read_requests: 1, max_read_request_operation_count: 1,
max_read_request_size: '1b', max_outstanding_read_requests: 1,
max_write_request_operation_count: 1, max_read_request_size: '1b',
max_write_request_size: '1b', max_write_request_operation_count: 1,
max_outstanding_write_requests: 1, max_write_request_size: '1b',
max_write_buffer_count: 1, max_outstanding_write_requests: 1,
max_write_buffer_size: '1b', max_write_buffer_count: 1,
max_retry_delay: '1s', max_write_buffer_size: '1b',
read_poll_timeout: '1s', max_retry_delay: '1s',
read_poll_timeout: '1s',
},
}, },
}, ],
], },
}; };
// These stats correlate to the above follower indices. // These stats correlate to the above follower indices.
const ccrFollowerIndexStatsMockResponse = { const ccrFollowerIndexStatsMockResponse = {
indices: [ body: {
{ indices: [
index: 'followerIndexName', {
shards: [ index: 'followerIndexName',
{ shards: [
shard_id: 1, {
leader_index: 'leaderIndex', shard_id: 1,
leader_global_checkpoint: 1, leader_index: 'leaderIndex',
leader_max_seq_no: 1, leader_global_checkpoint: 1,
follower_global_checkpoint: 1, leader_max_seq_no: 1,
follower_max_seq_no: 1, follower_global_checkpoint: 1,
last_requested_seq_no: 1, follower_max_seq_no: 1,
outstanding_read_requests: 1, last_requested_seq_no: 1,
outstanding_write_requests: 1, outstanding_read_requests: 1,
write_buffer_operation_count: 1, outstanding_write_requests: 1,
write_buffer_size_in_bytes: 1, write_buffer_operation_count: 1,
follower_mapping_version: 1, write_buffer_size_in_bytes: 1,
follower_settings_version: 1, follower_mapping_version: 1,
total_read_time_millis: 1, follower_settings_version: 1,
total_read_remote_exec_time_millis: 1, total_read_time_millis: 1,
successful_read_requests: 1, total_read_remote_exec_time_millis: 1,
failed_read_requests: 1, successful_read_requests: 1,
operations_read: 1, failed_read_requests: 1,
bytes_read: 1, operations_read: 1,
total_write_time_millis: 1, bytes_read: 1,
successful_write_requests: 1, total_write_time_millis: 1,
failed_write_requests: 1, successful_write_requests: 1,
operations_written: 1, failed_write_requests: 1,
read_exceptions: 1, operations_written: 1,
time_since_last_read_millis: 1, read_exceptions: 1,
}, time_since_last_read_millis: 1,
], },
}, ],
], },
],
},
}; };
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() followInfo: jest.fn().mockResolvedValueOnce(ccrInfoMockResponse),
.mockResolvedValueOnce(ccrInfoMockResponse) followStats: jest.fn().mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse),
.mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse), },
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -16,7 +16,7 @@ import { RouteDependencies } from '../../../types';
export const registerGetRoute = ({ export const registerGetRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ const paramsSchema = schema.object({
id: schema.string(), id: schema.string(),
@ -30,12 +30,13 @@ export const registerGetRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
try { try {
const { const {
follower_indices: followerIndices, body: { follower_indices: followerIndices },
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); } = await client.asCurrentUser.ccr.followInfo({ index: id });
const followerIndexInfo = followerIndices && followerIndices[0]; 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 this follower is paused, skip call to ES stats api since it will return 404
if (followerIndexInfo.status === 'paused') { if (followerIndexInfo.status === 'paused') {
return response.ok({ return response.ok({
// @ts-expect-error Once #98266 is merged, test this again.
body: deserializeFollowerIndex({ body: deserializeFollowerIndex({
...followerIndexInfo, ...followerIndexInfo,
}), }),
}); });
} else { } else {
const { const {
indices: followerIndicesStats, body: { indices: followerIndicesStats },
} = await context.crossClusterReplication!.client.callAsCurrentUser( } = await client.asCurrentUser.ccr.followStats({ index: id });
'ccr.followerIndexStats',
{ id }
);
return response.ok({ return response.ok({
// @ts-expect-error Once #98266 is merged, test this again.
body: deserializeFollowerIndex({ body: deserializeFollowerIndex({
...followerIndexInfo, ...followerIndexInfo,
...(followerIndicesStats ? followerIndicesStats[0] : {}), ...(followerIndicesStats ? followerIndicesStats[0] : {}),
}), }),
}); });
} }
} catch (err) { } catch (error) {
if (isEsError(err)) { return handleEsError({ error, response });
return response.customError(formatEsError(err));
}
// Case: default
throw err;
} }
}) })
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense, mockError } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerPauseRoute } from './register_pause_route'; import { registerPauseRoute } from './register_pause_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Pause follower index/indices', () => {
registerPauseRoute({ registerPauseRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,7 +33,9 @@ describe('[CCR API] Pause follower index/indices', () => {
it('pauses a single item', async () => { it('pauses a single item', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), ccr: {
pauseFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -51,11 +49,13 @@ describe('[CCR API] Pause follower index/indices', () => {
it('pauses multiple items', async () => { it('pauses multiple items', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseFollow: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }), .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -69,10 +69,12 @@ describe('[CCR API] Pause follower index/indices', () => {
it('returns partial errors', async () => { it('returns partial errors', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseFollow: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockRejectedValueOnce({ response: { error: {} } }), .mockResolvedValueOnce({ acknowledge: true })
.mockRejectedValueOnce(mockError),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerPauseRoute = ({ export const registerPauseRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ id: schema.string() }); const paramsSchema = schema.object({ id: schema.string() });
@ -27,29 +27,20 @@ export const registerPauseRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const ids = id.split(','); const ids = id.split(',');
const itemsPaused: string[] = []; const itemsPaused: string[] = [];
const errors: Array<{ id: string; error: any }> = []; 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( await Promise.all(
ids.map((_id: string) => ids.map((_id: string) =>
context client.asCurrentUser.ccr
.crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', { .pauseFollow({ index: _id })
id: _id,
})
.then(() => itemsPaused.push(_id)) .then(() => itemsPaused.push(_id))
.catch((err) => { .catch((error) => {
errors.push({ id: _id, error: formatError(err) }); errors.push({ id: _id, error: handleEsError({ error, response }) });
}) })
) )
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense, mockError } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerResumeRoute } from './register_resume_route'; import { registerResumeRoute } from './register_resume_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Resume follower index/indices', () => {
registerResumeRoute({ registerResumeRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,7 +33,9 @@ describe('[CCR API] Resume follower index/indices', () => {
it('resumes a single item', async () => { it('resumes a single item', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), ccr: {
resumeFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -51,11 +49,13 @@ describe('[CCR API] Resume follower index/indices', () => {
it('resumes multiple items', async () => { it('resumes multiple items', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() resumeFollow: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }), .mockResolvedValueOnce({ acknowledge: true })
.mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -69,10 +69,12 @@ describe('[CCR API] Resume follower index/indices', () => {
it('returns partial errors', async () => { it('returns partial errors', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() resumeFollow: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockRejectedValueOnce({ response: { error: {} } }), .mockResolvedValueOnce({ acknowledge: true })
.mockRejectedValueOnce(mockError),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerResumeRoute = ({ export const registerResumeRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ id: schema.string() }); const paramsSchema = schema.object({ id: schema.string() });
@ -27,29 +27,20 @@ export const registerResumeRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const ids = id.split(','); const ids = id.split(',');
const itemsResumed: string[] = []; const itemsResumed: string[] = [];
const errors: Array<{ id: string; error: any }> = []; 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( await Promise.all(
ids.map((_id: string) => ids.map((_id: string) =>
context client.asCurrentUser.ccr
.crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', { .resumeFollow({ index: _id })
id: _id,
})
.then(() => itemsResumed.push(_id)) .then(() => itemsResumed.push(_id))
.catch((err) => { .catch((error) => {
errors.push({ id: _id, error: formatError(err) }); errors.push({ id: _id, error: handleEsError({ error, response }) });
}) })
) )
); );

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError, License } from '../../../shared_imports'; import { handleEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error'; import { mockRouteContext, mockLicense, mockError } from '../test_lib';
import { mockRouteContext } from '../test_lib';
import { registerUnfollowRoute } from './register_unfollow_route'; import { registerUnfollowRoute } from './register_unfollow_route';
const httpService = httpServiceMock.createSetupContract(); const httpService = httpServiceMock.createSetupContract();
@ -23,12 +22,9 @@ describe('[CCR API] Unfollow follower index/indices', () => {
registerUnfollowRoute({ registerUnfollowRoute({
router, router,
license: { license: mockLicense,
guardApiRoute: (route: any) => route,
} as License,
lib: { lib: {
isEsError, handleEsError,
formatEsError,
}, },
}); });
@ -37,12 +33,14 @@ describe('[CCR API] Unfollow follower index/indices', () => {
it('unfollows a single item', async () => { it('unfollows a single item', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
.mockResolvedValueOnce({ acknowledge: true }) unfollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
.mockResolvedValueOnce({ acknowledge: true }) },
.mockResolvedValueOnce({ acknowledge: true }) indices: {
.mockResolvedValueOnce({ acknowledge: true }), close: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
open: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({
@ -56,23 +54,30 @@ describe('[CCR API] Unfollow follower index/indices', () => {
it('unfollows multiple items', async () => { it('unfollows multiple items', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseFollow: jest
// a .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }) // a
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }) // b
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }), // c
.mockResolvedValueOnce({ acknowledge: true }) unfollow: jest
// b .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }) // a
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }) // b
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }), // c
.mockResolvedValueOnce({ acknowledge: true }) },
// c indices: {
.mockResolvedValueOnce({ acknowledge: true }) close: jest
.mockResolvedValueOnce({ acknowledge: true }) .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }) // a
.mockResolvedValueOnce({ acknowledge: true }), .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({ const request = httpServerMock.createKibanaRequest({
@ -86,16 +91,20 @@ describe('[CCR API] Unfollow follower index/indices', () => {
it('returns partial errors', async () => { it('returns partial errors', async () => {
const routeContextMock = mockRouteContext({ const routeContextMock = mockRouteContext({
callAsCurrentUser: jest ccr: {
.fn() pauseFollow: jest
// a .fn()
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }) // a
.mockResolvedValueOnce({ acknowledge: true }) .mockResolvedValueOnce({ acknowledge: true }), // B
.mockResolvedValueOnce({ acknowledge: true }) unfollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), // a
.mockResolvedValueOnce({ acknowledge: true }) },
// b indices: {
.mockResolvedValueOnce({ acknowledge: true }) close: jest
.mockRejectedValueOnce({ response: { error: {} } }), .fn()
.mockResolvedValueOnce({ acknowledge: true }) // a
.mockRejectedValueOnce(mockError), // b
open: jest.fn().mockResolvedValueOnce({ acknowledge: true }), // a
},
}); });
const request = httpServerMock.createKibanaRequest({ const request = httpServerMock.createKibanaRequest({

View file

@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
export const registerUnfollowRoute = ({ export const registerUnfollowRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ id: schema.string() }); const paramsSchema = schema.object({ id: schema.string() });
@ -27,6 +27,7 @@ export const registerUnfollowRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
const ids = id.split(','); const ids = id.split(',');
@ -34,52 +35,34 @@ export const registerUnfollowRoute = ({
const itemsNotOpen: string[] = []; const itemsNotOpen: string[] = [];
const errors: Array<{ id: string; error: any }> = []; 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( await Promise.all(
ids.map(async (_id: string) => { ids.map(async (_id: string) => {
try { try {
// Try to pause follower, let it fail silently since it may already be paused // Try to pause follower, let it fail silently since it may already be paused
try { try {
await context.crossClusterReplication!.client.callAsCurrentUser( await client.asCurrentUser.ccr.pauseFollow({ index: _id });
'ccr.pauseFollowerIndex',
{ id: _id }
);
} catch (e) { } catch (e) {
// Swallow errors // Swallow errors
} }
// Close index // Close index
await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', { await client.asCurrentUser.indices.close({ index: _id });
index: _id,
});
// Unfollow leader // Unfollow leader
await context.crossClusterReplication!.client.callAsCurrentUser( await client.asCurrentUser.ccr.unfollow({ index: _id });
'ccr.unfollowLeaderIndex',
{ id: _id }
);
// Try to re-open the index, store failures in a separate array to surface warnings in the UI // 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 // This will allow users to query their index normally after unfollowing
try { try {
await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', { await client.asCurrentUser.indices.open({ index: _id });
index: _id,
});
} catch (e) { } catch (e) {
itemsNotOpen.push(_id); itemsNotOpen.push(_id);
} }
// Push success // Push success
itemsUnfollowed.push(_id); itemsUnfollowed.push(_id);
} catch (err) { } catch (error) {
errors.push({ id: _id, error: formatError(err) }); errors.push({ id: _id, error: handleEsError({ error, response }) });
} }
}) })
); );

View file

@ -0,0 +1,79 @@
/*
* 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.
*/
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { handleEsError } from '../../../shared_imports';
import { mockRouteContext, mockLicense } from '../test_lib';
import { registerUpdateRoute } from './register_update_route';
const httpService = httpServiceMock.createSetupContract();
describe('[CCR API] Update follower index', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
const router = httpService.createRouter();
registerUpdateRoute({
router,
license: mockLicense,
lib: {
handleEsError,
},
});
routeHandler = router.put.mock.calls[0][1];
});
it('should serialize the payload before sending it to Elasticsearch', async () => {
const routeContextMock = mockRouteContext({
ccr: {
followInfo: jest
.fn()
.mockResolvedValueOnce({ body: { follower_indices: [{ status: 'paused' }] } }),
// Just echo back what we send so we can inspect it.
resumeFollow: jest.fn().mockImplementation((payload) => ({ body: payload })),
},
});
const request = httpServerMock.createKibanaRequest({
params: { id: 'foo' },
body: {
maxReadRequestOperationCount: 1,
maxOutstandingReadRequests: 1,
maxReadRequestSize: '1b',
maxWriteRequestOperationCount: 1,
maxWriteRequestSize: '1b',
maxOutstandingWriteRequests: 1,
maxWriteBufferCount: 1,
maxWriteBufferSize: '1b',
maxRetryDelay: '1s',
readPollTimeout: '1s',
},
});
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
expect(response.payload).toEqual({
index: 'foo',
body: {
max_outstanding_read_requests: 1,
max_outstanding_write_requests: 1,
max_read_request_operation_count: 1,
max_read_request_size: '1b',
max_retry_delay: '1s',
max_write_buffer_count: 1,
max_write_buffer_size: '1b',
max_write_request_operation_count: 1,
max_write_request_size: '1b',
read_poll_timeout: '1s',
},
});
});
});

View file

@ -18,7 +18,7 @@ import { RouteDependencies } from '../../../types';
export const registerUpdateRoute = ({ export const registerUpdateRoute = ({
router, router,
license, license,
lib: { isEsError, formatEsError }, lib: { handleEsError },
}: RouteDependencies) => { }: RouteDependencies) => {
const paramsSchema = schema.object({ id: schema.string() }); const paramsSchema = schema.object({ id: schema.string() });
@ -44,13 +44,14 @@ export const registerUpdateRoute = ({
}, },
}, },
license.guardApiRoute(async (context, request, response) => { license.guardApiRoute(async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { id } = request.params; const { id } = request.params;
// We need to first pause the follower and then resume it by passing the advanced settings // We need to first pause the follower and then resume it by passing the advanced settings
try { try {
const { const {
follower_indices: followerIndices, body: { follower_indices: followerIndices },
} = await context.crossClusterReplication.client.callAsCurrentUser('ccr.info', { id }); } = await client.asCurrentUser.ccr.followInfo({ index: id });
const followerIndexInfo = followerIndices && followerIndices[0]; const followerIndexInfo = followerIndices && followerIndices[0];
@ -63,12 +64,7 @@ export const registerUpdateRoute = ({
// Pause follower if not already paused // Pause follower if not already paused
if (!isPaused) { if (!isPaused) {
await context.crossClusterReplication!.client.callAsCurrentUser( await client.asCurrentUser.ccr.pauseFollow({ index: id });
'ccr.pauseFollowerIndex',
{
id,
}
);
} }
// Resume follower // Resume follower
@ -76,18 +72,16 @@ export const registerUpdateRoute = ({
serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings) serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings)
); );
return response.ok({ const { body: responseBody } = await client.asCurrentUser.ccr.resumeFollow({
body: await context.crossClusterReplication!.client.callAsCurrentUser( index: id,
'ccr.resumeFollowerIndex', body,
{ id, body }
),
}); });
} catch (err) {
if (isEsError(err)) { return response.ok({
return response.customError(formatEsError(err)); body: responseBody,
} });
// Case: default } catch (error) {
throw err; return handleEsError({ error, response });
} }
}) })
); );

View file

@ -6,19 +6,24 @@
*/ */
import { RequestHandlerContext } from 'src/core/server'; import { RequestHandlerContext } from 'src/core/server';
import { License } from '../../shared_imports';
export function mockRouteContext({ export function mockRouteContext(mockedFunctions: unknown): RequestHandlerContext {
callAsCurrentUser,
}: {
callAsCurrentUser: any;
}): RequestHandlerContext {
const routeContextMock = ({ const routeContextMock = ({
crossClusterReplication: { core: {
client: { elasticsearch: {
callAsCurrentUser, client: {
asCurrentUser: mockedFunctions,
},
}, },
}, },
} as unknown) as RequestHandlerContext; } as unknown) as RequestHandlerContext;
return routeContextMock; return routeContextMock;
} }
export const mockLicense = {
guardApiRoute: (route: any) => route,
} as License;
export const mockError = { name: 'ResponseError', statusCode: 400 };

View file

@ -5,5 +5,5 @@
* 2.0. * 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'; export { License } from '../../license_api_guard/server';

View file

@ -5,13 +5,12 @@
* 2.0. * 2.0.
*/ */
import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; import { IRouter } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { IndexManagementPluginSetup } from '../../index_management/server'; import { IndexManagementPluginSetup } from '../../index_management/server';
import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
import { License, isEsError } from './shared_imports'; import { License, handleEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
export interface SetupDependencies { export interface SetupDependencies {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
@ -25,24 +24,9 @@ export interface StartDependencies {
} }
export interface RouteDependencies { export interface RouteDependencies {
router: CcrPluginRouter; router: IRouter;
license: License; license: License;
lib: { lib: {
isEsError: typeof isEsError; handleEsError: typeof handleEsError;
formatEsError: typeof formatEsError;
}; };
} }
/**
* @internal
*/
export interface CcrRequestHandlerContext extends RequestHandlerContext {
crossClusterReplication: {
client: ILegacyScopedClusterClient;
};
}
/**
* @internal
*/
type CcrPluginRouter = IRouter<CcrRequestHandlerContext>;

View file

@ -45,7 +45,7 @@ export default function ({ getService }) {
}); });
expect(body.statusCode).to.be(404); expect(body.statusCode).to.be(404);
expect(body.attributes.cause[0]).to.contain('no such remote cluster'); expect(body.attributes.error.reason).to.contain('no such remote cluster');
}); });
}); });
@ -70,7 +70,7 @@ export default function ({ getService }) {
it('should return a 404 when the auto-follow pattern is not found', async () => { it('should return a 404 when the auto-follow pattern is not found', async () => {
const { body } = await getAutoFollowPattern('missing-pattern'); const { body } = await getAutoFollowPattern('missing-pattern');
expect(body.statusCode).to.be(404); expect(body.statusCode).to.be(404);
expect(body.attributes.cause).not.to.be(undefined); expect(body.attributes.error.reason).not.to.be(undefined);
}); });
it('should return an auto-follow pattern that was created', async () => { it('should return an auto-follow pattern that was created', async () => {

View file

@ -6,10 +6,9 @@
*/ */
import { REMOTE_CLUSTER_NAME } from './constants'; import { REMOTE_CLUSTER_NAME } from './constants';
import { getRandomString } from './lib';
export const getFollowerIndexPayload = ( export const getFollowerIndexPayload = (
leaderIndexName = getRandomString(), leaderIndexName = 'test-leader-index',
remoteCluster = REMOTE_CLUSTER_NAME, remoteCluster = REMOTE_CLUSTER_NAME,
advancedSettings = {} advancedSettings = {}
) => ({ ) => ({

View file

@ -6,7 +6,6 @@
*/ */
import { API_BASE_PATH } from './constants'; import { API_BASE_PATH } from './constants';
import { getRandomString } from './lib';
import { getFollowerIndexPayload } from './fixtures'; import { getFollowerIndexPayload } from './fixtures';
export const registerHelpers = (supertest) => { export const registerHelpers = (supertest) => {
@ -51,7 +50,7 @@ export const registerHelpers = (supertest) => {
}; };
}; };
const createFollowerIndex = (name = getRandomString(), payload = getFollowerIndexPayload()) => { const createFollowerIndex = (name, payload = getFollowerIndexPayload()) => {
followerIndicesCreated.push(name); followerIndicesCreated.push(name);
return supertest return supertest

View file

@ -9,7 +9,7 @@ import expect from '@kbn/expect';
import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants'; import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants';
import { getFollowerIndexPayload } from './fixtures'; import { getFollowerIndexPayload } from './fixtures';
import { registerHelpers as registerElasticSearchHelpers, getRandomString } from './lib'; import { registerHelpers as registerElasticSearchHelpers } from './lib';
import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers';
import { registerHelpers as registerFollowerIndicesnHelpers } from './follower_indices.helpers'; import { registerHelpers as registerFollowerIndicesnHelpers } from './follower_indices.helpers';
@ -47,23 +47,23 @@ export default function ({ getService }) {
const payload = getFollowerIndexPayload(); const payload = getFollowerIndexPayload();
payload.remoteCluster = 'unknown-cluster'; payload.remoteCluster = 'unknown-cluster';
const { body } = await createFollowerIndex(undefined, payload).expect(404); const { body } = await createFollowerIndex('test', payload).expect(404);
expect(body.attributes.cause[0]).to.contain('no such remote cluster'); expect(body.attributes.error.reason).to.contain('no such remote cluster');
}); });
it('should throw a 404 error trying to follow an unknown index', async () => { it('should throw a 404 error trying to follow an unknown index', async () => {
const payload = getFollowerIndexPayload(); const payload = getFollowerIndexPayload();
const { body } = await createFollowerIndex(undefined, payload).expect(404); const { body } = await createFollowerIndex('test', payload).expect(404);
expect(body.attributes.cause[0]).to.contain('no such index'); expect(body.attributes.error.reason).to.contain('no such index');
}); });
// NOTE: If this test fails locally it's probably because you have another cluster running. // NOTE: If this test fails locally it's probably because you have another cluster running.
it('should create a follower index that follows an existing leader index', async () => { it('should create a follower index that follows an existing leader index', async () => {
// First let's create an index to follow // First let's create an index to follow
const leaderIndex = await createIndex(); const leaderIndex = await createIndex('leader1');
const payload = getFollowerIndexPayload(leaderIndex); const payload = getFollowerIndexPayload(leaderIndex);
const { body } = await createFollowerIndex(undefined, payload).expect(200); const { body } = await createFollowerIndex('index1', payload).expect(200);
// There is a race condition in which Elasticsearch can respond without acknowledging, // There is a race condition in which Elasticsearch can respond without acknowledging,
// i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false. // i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false.
@ -74,17 +74,17 @@ export default function ({ getService }) {
describe('get()', () => { describe('get()', () => {
it('should return a 404 when the follower index does not exist', async () => { it('should return a 404 when the follower index does not exist', async () => {
const name = getRandomString(); const name = 'test';
const { body } = await getFollowerIndex(name).expect(404); const { body } = await getFollowerIndex(name).expect(404);
expect(body.attributes.cause[0]).to.contain('no such index'); expect(body.attributes.error.reason).to.contain('no such index');
}); });
// NOTE: If this test fails locally it's probably because you have another cluster running. // NOTE: If this test fails locally it's probably because you have another cluster running.
it('should return a follower index that was created', async () => { it('should return a follower index that was created', async () => {
const leaderIndex = await createIndex(); const leaderIndex = await createIndex('leader2');
const name = getRandomString(); const name = 'index2';
const payload = getFollowerIndexPayload(leaderIndex); const payload = getFollowerIndexPayload(leaderIndex);
await createFollowerIndex(name, payload); await createFollowerIndex(name, payload);
@ -98,8 +98,8 @@ export default function ({ getService }) {
describe('update()', () => { describe('update()', () => {
it('should update a follower index advanced settings', async () => { it('should update a follower index advanced settings', async () => {
// Create a follower index // Create a follower index
const leaderIndex = await createIndex(); const leaderIndex = await createIndex('leader3');
const followerIndex = getRandomString(); const followerIndex = 'index3';
const initialValue = 1234; const initialValue = 1234;
const payload = getFollowerIndexPayload(leaderIndex, undefined, { const payload = getFollowerIndexPayload(leaderIndex, undefined, {
maxReadRequestOperationCount: initialValue, maxReadRequestOperationCount: initialValue,
@ -128,9 +128,8 @@ export default function ({ getService }) {
* When we then retrieve the follower index it will have all the advanced settings * When we then retrieve the follower index it will have all the advanced settings
* coming from ES. We can then compare those settings with our hard-coded values. * coming from ES. We can then compare those settings with our hard-coded values.
*/ */
const leaderIndex = await createIndex(); const leaderIndex = await createIndex('leader4');
const name = 'index4';
const name = getRandomString();
const payload = getFollowerIndexPayload(leaderIndex); const payload = getFollowerIndexPayload(leaderIndex);
await createFollowerIndex(name, payload); await createFollowerIndex(name, payload);

View file

@ -5,20 +5,18 @@
* 2.0. * 2.0.
*/ */
import { getRandomString } from './random';
/** /**
* Helpers to create and delete indices on the Elasticsearch instance * Helpers to create and delete indices on the Elasticsearch instance
* during our tests. * during our tests.
* @param {ElasticsearchClient} es The Elasticsearch client instance * @param {ElasticsearchClient} es The Elasticsearch client instance
*/ */
export const registerHelpers = (getService) => { export const registerHelpers = (getService) => {
const es = getService('legacyEs'); const es = getService('es');
const esDeleteAllIndices = getService('esDeleteAllIndices'); const esDeleteAllIndices = getService('esDeleteAllIndices');
let indicesCreated = []; let indicesCreated = [];
const createIndex = (index = getRandomString()) => { const createIndex = (index) => {
indicesCreated.push(index); indicesCreated.push(index);
return es.indices.create({ index }).then(() => index); return es.indices.create({ index }).then(() => index);
}; };

View file

@ -6,5 +6,3 @@
*/ */
export { registerHelpers } from './elasticsearch'; export { registerHelpers } from './elasticsearch';
export { getRandomString } from './random';

View file

@ -1,13 +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.
*/
import Chance from 'chance';
const chance = new Chance();
const CHARS_POOL = 'abcdefghijklmnopqrstuvwxyz';
export const getRandomString = () => `${chance.string({ pool: CHARS_POOL })}-${Date.now()}`;