* 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:
parent
d6cb25aeea
commit
e044137bd4
47 changed files with 652 additions and 977 deletions
|
@ -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`.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 }) });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 }) });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 }) });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 }) });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 }) });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 }) });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 = {}
|
||||
) => ({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -6,5 +6,3 @@
|
|||
*/
|
||||
|
||||
export { registerHelpers } from './elasticsearch';
|
||||
|
||||
export { getRandomString } from './random';
|
||||
|
|
|
@ -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()}`;
|
Loading…
Reference in a new issue