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.
1. Start your "local" cluster by running `yarn es snapshot --license=trial` and `yarn start` to start Kibana.
2. Start your "remote" cluster by running `yarn es snapshot --license=trial -E cluster.name=europe -E transport.port=9400` in a separate terminal tab.
3. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`.
Note that these settings are required for testing auto-follow pattern conflicts errors (see below).
1. Ensure Kibana isn't running so it doesn't load up any data into your cluster. Run `yarn es snapshot --license=trial` to install a fresh snapshot. Wait for ES to finish setting up.
2. Create a "remote" copy of your ES snapshot by running: `cp -R .es/8.0.0 .es/8.0.0-2`.
4. Start your "remote" cluster by running `.es/8.0.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400`.
4. Run `yarn start` to start Kibana.
5. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`. Note that these settings are required for testing auto-follow pattern conflicts errors (see below).
Now you can create follower indices and auto-follow patterns to replicate the `my-leader-index`
index on the remote cluster that's available at `127.0.0.1:9400`.

View file

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

View file

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

View file

@ -121,24 +121,24 @@ export class FollowerIndexEdit extends PureComponent {
params: { id: name },
},
} = this.props;
const title = i18n.translate(
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle',
{
defaultMessage: 'Error loading follower index',
}
);
const errorMessage =
error.status === 404
error.body.statusCode === 404
? {
data: {
error: i18n.translate(
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
{
defaultMessage: `The follower index '{name}' does not exist.`,
values: { name },
}
),
},
error: i18n.translate(
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
{
defaultMessage: `The follower index '{name}' does not exist.`,
values: { name },
}
),
}
: 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 {
CoreSetup,
CoreStart,
ILegacyCustomClusterClient,
Plugin,
Logger,
PluginInitializerContext,
@ -19,20 +18,12 @@ import {
import { Index } from '../../index_management/server';
import { PLUGIN } from '../common/constants';
import { SetupDependencies, StartDependencies, CcrRequestHandlerContext } from './types';
import { SetupDependencies, StartDependencies } from './types';
import { registerApiRoutes } from './routes';
import { elasticsearchJsPlugin } from './client/elasticsearch_ccr';
import { CrossClusterReplicationConfig } from './config';
import { License, isEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
const [core] = await getStartServices();
// Extend the elasticsearchJs client with additional endpoints.
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
return core.elasticsearch.legacy.createClient('crossClusterReplication', esClientConfig);
}
import { License, handleEsError } from './shared_imports';
// TODO replace deprecated ES client after Index Management is updated
const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => {
if (!indicesList?.length) {
return indicesList;
@ -66,7 +57,6 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
private readonly config$: Observable<CrossClusterReplicationConfig>;
private readonly license: License;
private readonly logger: Logger;
private ccrEsClient?: ILegacyCustomClusterClient;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
@ -114,22 +104,11 @@ export class CrossClusterReplicationServerPlugin implements Plugin<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({
router: http.createRouter(),
license: this.license,
lib: {
isEsError,
formatEsError,
handleEsError,
},
});
}
@ -142,9 +121,5 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
});
}
stop() {
if (this.ccrEsClient) {
this.ccrEsClient.close();
}
}
stop() {}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
export { handleEsError } from '../../../../src/plugins/es_ui_shared/server';
export { License } from '../../license_api_guard/server';

View file

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

View file

@ -45,7 +45,7 @@ export default function ({ getService }) {
});
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 () => {
const { body } = await getAutoFollowPattern('missing-pattern');
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 () => {

View file

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

View file

@ -6,7 +6,6 @@
*/
import { API_BASE_PATH } from './constants';
import { getRandomString } from './lib';
import { getFollowerIndexPayload } from './fixtures';
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);
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 { 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 registerFollowerIndicesnHelpers } from './follower_indices.helpers';
@ -47,23 +47,23 @@ export default function ({ getService }) {
const payload = getFollowerIndexPayload();
payload.remoteCluster = 'unknown-cluster';
const { body } = await createFollowerIndex(undefined, payload).expect(404);
expect(body.attributes.cause[0]).to.contain('no such remote cluster');
const { body } = await createFollowerIndex('test', payload).expect(404);
expect(body.attributes.error.reason).to.contain('no such remote cluster');
});
it('should throw a 404 error trying to follow an unknown index', async () => {
const payload = getFollowerIndexPayload();
const { body } = await createFollowerIndex(undefined, payload).expect(404);
expect(body.attributes.cause[0]).to.contain('no such index');
const { body } = await createFollowerIndex('test', payload).expect(404);
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.
it('should create a follower index that follows an existing leader index', async () => {
// First let's create an index to follow
const leaderIndex = await createIndex();
const leaderIndex = await createIndex('leader1');
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,
// i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false.
@ -74,17 +74,17 @@ export default function ({ getService }) {
describe('get()', () => {
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);
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.
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);
await createFollowerIndex(name, payload);
@ -98,8 +98,8 @@ export default function ({ getService }) {
describe('update()', () => {
it('should update a follower index advanced settings', async () => {
// Create a follower index
const leaderIndex = await createIndex();
const followerIndex = getRandomString();
const leaderIndex = await createIndex('leader3');
const followerIndex = 'index3';
const initialValue = 1234;
const payload = getFollowerIndexPayload(leaderIndex, undefined, {
maxReadRequestOperationCount: initialValue,
@ -128,9 +128,8 @@ export default function ({ getService }) {
* 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.
*/
const leaderIndex = await createIndex();
const name = getRandomString();
const leaderIndex = await createIndex('leader4');
const name = 'index4';
const payload = getFollowerIndexPayload(leaderIndex);
await createFollowerIndex(name, payload);

View file

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

View file

@ -6,5 +6,3 @@
*/
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()}`;