* 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.
|
You can run a local cluster and simulate a remote cluster within a single Kibana directory.
|
||||||
|
|
||||||
1. Start your "local" cluster by running `yarn es snapshot --license=trial` and `yarn start` to start Kibana.
|
1. Ensure Kibana isn't running so it doesn't load up any data into your cluster. Run `yarn es snapshot --license=trial` to install a fresh snapshot. Wait for ES to finish setting up.
|
||||||
2. Start your "remote" cluster by running `yarn es snapshot --license=trial -E cluster.name=europe -E transport.port=9400` in a separate terminal tab.
|
2. Create a "remote" copy of your ES snapshot by running: `cp -R .es/8.0.0 .es/8.0.0-2`.
|
||||||
3. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`.
|
4. Start your "remote" cluster by running `.es/8.0.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400`.
|
||||||
Note that these settings are required for testing auto-follow pattern conflicts errors (see below).
|
4. Run `yarn start` to start Kibana.
|
||||||
|
5. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`. Note that these settings are required for testing auto-follow pattern conflicts errors (see below).
|
||||||
|
|
||||||
Now you can create follower indices and auto-follow patterns to replicate the `my-leader-index`
|
Now you can create follower indices and auto-follow patterns to replicate the `my-leader-index`
|
||||||
index on the remote cluster that's available at `127.0.0.1:9400`.
|
index on the remote cluster that's available at `127.0.0.1:9400`.
|
||||||
|
|
|
@ -11,21 +11,19 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||||
export function SectionError(props) {
|
export function SectionError(props) {
|
||||||
const { title, error, ...rest } = props;
|
const { title, error, ...rest } = props;
|
||||||
const data = error.body ? error.body : error;
|
const data = error.body ? error.body : error;
|
||||||
const {
|
const { error: errorString, attributes, message } = data;
|
||||||
error: errorString,
|
|
||||||
attributes, // wrapEsError() on the server add a "cause" array
|
|
||||||
message,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
|
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
|
||||||
<div>{message || errorString}</div>
|
<div>{message || errorString}</div>
|
||||||
{attributes && attributes.cause && (
|
{attributes?.error?.root_cause && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<ul>
|
<ul>
|
||||||
{attributes.cause.map((message, i) => (
|
{attributes.error.root_cause.map(({ type, reason }, i) => (
|
||||||
<li key={i}>{message}</li>
|
<li key={i}>
|
||||||
|
{type}: {reason}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
|
@ -79,24 +79,24 @@ export class AutoFollowPatternEdit extends PureComponent {
|
||||||
params: { id: name },
|
params: { id: name },
|
||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const title = i18n.translate(
|
const title = i18n.translate(
|
||||||
'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle',
|
'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Error loading auto-follow pattern',
|
defaultMessage: 'Error loading auto-follow pattern',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.status === 404
|
error.body.statusCode === 404
|
||||||
? {
|
? {
|
||||||
data: {
|
error: i18n.translate(
|
||||||
error: i18n.translate(
|
'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorMessage',
|
||||||
'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorMessage',
|
{
|
||||||
{
|
defaultMessage: `The auto-follow pattern '{name}' does not exist.`,
|
||||||
defaultMessage: `The auto-follow pattern '{name}' does not exist.`,
|
values: { name },
|
||||||
values: { name },
|
}
|
||||||
}
|
),
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: error;
|
: error;
|
||||||
|
|
||||||
|
|
|
@ -121,24 +121,24 @@ export class FollowerIndexEdit extends PureComponent {
|
||||||
params: { id: name },
|
params: { id: name },
|
||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const title = i18n.translate(
|
const title = i18n.translate(
|
||||||
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle',
|
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Error loading follower index',
|
defaultMessage: 'Error loading follower index',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.status === 404
|
error.body.statusCode === 404
|
||||||
? {
|
? {
|
||||||
data: {
|
error: i18n.translate(
|
||||||
error: i18n.translate(
|
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
|
||||||
'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
|
{
|
||||||
{
|
defaultMessage: `The follower index '{name}' does not exist.`,
|
||||||
defaultMessage: `The follower index '{name}' does not exist.`,
|
values: { name },
|
||||||
values: { name },
|
}
|
||||||
}
|
),
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: error;
|
: error;
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
import {
|
||||||
CoreSetup,
|
CoreSetup,
|
||||||
CoreStart,
|
CoreStart,
|
||||||
ILegacyCustomClusterClient,
|
|
||||||
Plugin,
|
Plugin,
|
||||||
Logger,
|
Logger,
|
||||||
PluginInitializerContext,
|
PluginInitializerContext,
|
||||||
|
@ -19,20 +18,12 @@ import {
|
||||||
|
|
||||||
import { Index } from '../../index_management/server';
|
import { Index } from '../../index_management/server';
|
||||||
import { PLUGIN } from '../common/constants';
|
import { PLUGIN } from '../common/constants';
|
||||||
import { SetupDependencies, StartDependencies, CcrRequestHandlerContext } from './types';
|
import { SetupDependencies, StartDependencies } from './types';
|
||||||
import { registerApiRoutes } from './routes';
|
import { registerApiRoutes } from './routes';
|
||||||
import { elasticsearchJsPlugin } from './client/elasticsearch_ccr';
|
|
||||||
import { CrossClusterReplicationConfig } from './config';
|
import { CrossClusterReplicationConfig } from './config';
|
||||||
import { License, isEsError } from './shared_imports';
|
import { License, handleEsError } from './shared_imports';
|
||||||
import { formatEsError } from './lib/format_es_error';
|
|
||||||
|
|
||||||
async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
|
|
||||||
const [core] = await getStartServices();
|
|
||||||
// Extend the elasticsearchJs client with additional endpoints.
|
|
||||||
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
|
|
||||||
return core.elasticsearch.legacy.createClient('crossClusterReplication', esClientConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO replace deprecated ES client after Index Management is updated
|
||||||
const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => {
|
const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => {
|
||||||
if (!indicesList?.length) {
|
if (!indicesList?.length) {
|
||||||
return indicesList;
|
return indicesList;
|
||||||
|
@ -66,7 +57,6 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
|
||||||
private readonly config$: Observable<CrossClusterReplicationConfig>;
|
private readonly config$: Observable<CrossClusterReplicationConfig>;
|
||||||
private readonly license: License;
|
private readonly license: License;
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
private ccrEsClient?: ILegacyCustomClusterClient;
|
|
||||||
|
|
||||||
constructor(initializerContext: PluginInitializerContext) {
|
constructor(initializerContext: PluginInitializerContext) {
|
||||||
this.logger = initializerContext.logger.get();
|
this.logger = initializerContext.logger.get();
|
||||||
|
@ -114,22 +104,11 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
http.registerRouteHandlerContext<CcrRequestHandlerContext, 'crossClusterReplication'>(
|
|
||||||
'crossClusterReplication',
|
|
||||||
async (ctx, request) => {
|
|
||||||
this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices));
|
|
||||||
return {
|
|
||||||
client: this.ccrEsClient.asScoped(request),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
registerApiRoutes({
|
registerApiRoutes({
|
||||||
router: http.createRouter(),
|
router: http.createRouter(),
|
||||||
license: this.license,
|
license: this.license,
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -142,9 +121,5 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {}
|
||||||
if (this.ccrEsClient) {
|
|
||||||
this.ccrEsClient.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerCreateRoute } from './register_create_route';
|
import { registerCreateRoute } from './register_create_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Create auto-follow pattern', () => {
|
||||||
|
|
||||||
registerCreateRoute({
|
registerCreateRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,8 +33,10 @@ describe('[CCR API] Create auto-follow pattern', () => {
|
||||||
|
|
||||||
it('should throw a 409 conflict error if id already exists', async () => {
|
it('should throw a 409 conflict error if id already exists', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
// Fail the uniqueness check.
|
ccr: {
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce(true),
|
// Fail the uniqueness check.
|
||||||
|
getAutoFollowPattern: jest.fn().mockResolvedValueOnce(true),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -54,11 +52,11 @@ describe('[CCR API] Create auto-follow pattern', () => {
|
||||||
|
|
||||||
it('should return 200 status when the id does not exist', async () => {
|
it('should return 200 status when the id does not exist', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
|
||||||
// Pass the uniqueness check.
|
// Pass the uniqueness check.
|
||||||
.mockRejectedValueOnce({ statusCode: 404 })
|
getAutoFollowPattern: jest.fn().mockRejectedValueOnce({ statusCode: 404 }),
|
||||||
.mockResolvedValueOnce(true),
|
putAutoFollowPattern: jest.fn().mockResolvedValueOnce(true),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerCreateRoute = ({
|
export const registerCreateRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const bodySchema = schema.object({
|
const bodySchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -34,6 +34,7 @@ export const registerCreateRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id, ...rest } = request.body;
|
const { id, ...rest } = request.body;
|
||||||
const body = serializeAutoFollowPattern(rest as AutoFollowPattern);
|
const body = serializeAutoFollowPattern(rest as AutoFollowPattern);
|
||||||
|
|
||||||
|
@ -42,36 +43,29 @@ export const registerCreateRoute = ({
|
||||||
* the same id does not exist.
|
* the same id does not exist.
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', {
|
await client.asCurrentUser.ccr.getAutoFollowPattern({ name: id });
|
||||||
id,
|
|
||||||
});
|
|
||||||
// If we get here it means that an auto-follow pattern with the same id exists
|
// If we get here it means that an auto-follow pattern with the same id exists
|
||||||
return response.conflict({
|
return response.conflict({
|
||||||
body: `An auto-follow pattern with the name "${id}" already exists.`,
|
body: `An auto-follow pattern with the name "${id}" already exists.`,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (err.statusCode !== 404) {
|
if (error.statusCode !== 404) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return response.ok({
|
const { body: responseBody } = await client.asCurrentUser.ccr.putAutoFollowPattern({
|
||||||
body: await context.crossClusterReplication!.client.callAsCurrentUser(
|
name: id,
|
||||||
'ccr.saveAutoFollowPattern',
|
body,
|
||||||
{ id, body }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
if (isEsError(err)) {
|
return response.ok({
|
||||||
return response.customError(formatEsError(err));
|
body: responseBody,
|
||||||
}
|
});
|
||||||
// Case: default
|
} catch (error) {
|
||||||
throw err;
|
return handleEsError({ error, response });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense, mockError } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerDeleteRoute } from './register_delete_route';
|
import { registerDeleteRoute } from './register_delete_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
registerDeleteRoute({
|
registerDeleteRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +33,9 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('deletes a single item', async () => {
|
it('deletes a single item', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
ccr: {
|
||||||
|
deleteAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -51,11 +49,13 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('deletes multiple items', async () => {
|
it('deletes multiple items', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
deleteAutoFollowPattern: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -69,10 +69,12 @@ describe('[CCR API] Delete auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('returns partial errors', async () => {
|
it('returns partial errors', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
deleteAutoFollowPattern: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockRejectedValueOnce({ response: { error: {} } }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockRejectedValueOnce(mockError),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerDeleteRoute = ({
|
export const registerDeleteRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -30,29 +30,22 @@ export const registerDeleteRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const ids = id.split(',');
|
const ids = id.split(',');
|
||||||
|
|
||||||
const itemsDeleted: string[] = [];
|
const itemsDeleted: string[] = [];
|
||||||
const errors: Array<{ id: string; error: any }> = [];
|
const errors: Array<{ id: string; error: any }> = [];
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
|
||||||
if (isEsError(err)) {
|
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
return response.customError({ statusCode: 500, body: err });
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map((_id) =>
|
ids.map((_id) =>
|
||||||
context
|
client.asCurrentUser.ccr
|
||||||
.crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', {
|
.deleteAutoFollowPattern({
|
||||||
id: _id,
|
name: _id,
|
||||||
})
|
})
|
||||||
.then(() => itemsDeleted.push(_id))
|
.then(() => itemsDeleted.push(_id))
|
||||||
.catch((err: any) => {
|
.catch((error: any) => {
|
||||||
errors.push({ id: _id, error: formatError(err) });
|
errors.push({ id: _id, error: handleEsError({ error, response }) });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerFetchRoute } from './register_fetch_route';
|
import { registerFetchRoute } from './register_fetch_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Fetch all auto-follow patterns', () => {
|
||||||
|
|
||||||
registerFetchRoute({
|
registerFetchRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,21 +33,25 @@ describe('[CCR API] Fetch all auto-follow patterns', () => {
|
||||||
|
|
||||||
it('deserializes the response from Elasticsearch', async () => {
|
it('deserializes the response from Elasticsearch', async () => {
|
||||||
const ccrAutoFollowPatternResponseMock = {
|
const ccrAutoFollowPatternResponseMock = {
|
||||||
patterns: [
|
body: {
|
||||||
{
|
patterns: [
|
||||||
name: 'autoFollowPattern',
|
{
|
||||||
pattern: {
|
name: 'autoFollowPattern',
|
||||||
active: true,
|
pattern: {
|
||||||
remote_cluster: 'remoteCluster',
|
active: true,
|
||||||
leader_index_patterns: ['leader*'],
|
remote_cluster: 'remoteCluster',
|
||||||
follow_index_pattern: 'follow',
|
leader_index_patterns: ['leader*'],
|
||||||
|
follow_index_pattern: 'follow',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
|
ccr: {
|
||||||
|
getAutoFollowPattern: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest();
|
const request = httpServerMock.createKibanaRequest();
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerFetchRoute = ({
|
export const registerFetchRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
|
@ -23,21 +23,20 @@ export const registerFetchRoute = ({
|
||||||
validate: false,
|
validate: false,
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await context.crossClusterReplication!.client.callAsCurrentUser(
|
const {
|
||||||
'ccr.autoFollowPatterns'
|
body: { patterns },
|
||||||
);
|
} = await client.asCurrentUser.ccr.getAutoFollowPattern();
|
||||||
return response.ok({
|
return response.ok({
|
||||||
body: {
|
body: {
|
||||||
patterns: deserializeListAutoFollowPatterns(result.patterns),
|
// @ts-expect-error Once #98266 is merged, test this again.
|
||||||
|
patterns: deserializeListAutoFollowPatterns(patterns),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerGetRoute } from './register_get_route';
|
import { registerGetRoute } from './register_get_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Get one auto-follow pattern', () => {
|
||||||
|
|
||||||
registerGetRoute({
|
registerGetRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,21 +33,25 @@ describe('[CCR API] Get one auto-follow pattern', () => {
|
||||||
|
|
||||||
it('should return a single resource even though ES returns an array with 1 item', async () => {
|
it('should return a single resource even though ES returns an array with 1 item', async () => {
|
||||||
const ccrAutoFollowPatternResponseMock = {
|
const ccrAutoFollowPatternResponseMock = {
|
||||||
patterns: [
|
body: {
|
||||||
{
|
patterns: [
|
||||||
name: 'autoFollowPattern',
|
{
|
||||||
pattern: {
|
name: 'autoFollowPattern',
|
||||||
active: true,
|
pattern: {
|
||||||
remote_cluster: 'remoteCluster',
|
active: true,
|
||||||
leader_index_patterns: ['leader*'],
|
remote_cluster: 'remoteCluster',
|
||||||
follow_index_pattern: 'follow',
|
leader_index_patterns: ['leader*'],
|
||||||
|
follow_index_pattern: 'follow',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
|
ccr: {
|
||||||
|
getAutoFollowPattern: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest();
|
const request = httpServerMock.createKibanaRequest();
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerGetRoute = ({
|
export const registerGetRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -31,24 +31,22 @@ export const registerGetRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await context.crossClusterReplication!.client.callAsCurrentUser(
|
const result = await client.asCurrentUser.ccr.getAutoFollowPattern({
|
||||||
'ccr.autoFollowPattern',
|
name: id,
|
||||||
{ id }
|
});
|
||||||
);
|
|
||||||
const autoFollowPattern = result.patterns[0];
|
const autoFollowPattern = result.body.patterns[0];
|
||||||
|
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
// @ts-expect-error Once #98266 is merged, test this again.
|
||||||
body: deserializeAutoFollowPattern(autoFollowPattern),
|
body: deserializeAutoFollowPattern(autoFollowPattern),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense, mockError } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerPauseRoute } from './register_pause_route';
|
import { registerPauseRoute } from './register_pause_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
registerPauseRoute({
|
registerPauseRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +33,9 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('pauses a single item', async () => {
|
it('pauses a single item', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
ccr: {
|
||||||
|
pauseAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -51,11 +49,13 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('pauses multiple items', async () => {
|
it('pauses multiple items', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseAutoFollowPattern: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -69,10 +69,12 @@ describe('[CCR API] Pause auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('returns partial errors', async () => {
|
it('returns partial errors', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseAutoFollowPattern: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockRejectedValueOnce({ response: { error: {} } }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockRejectedValueOnce(mockError),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerPauseRoute = ({
|
export const registerPauseRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -29,29 +29,22 @@ export const registerPauseRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const ids = id.split(',');
|
const ids = id.split(',');
|
||||||
|
|
||||||
const itemsPaused: string[] = [];
|
const itemsPaused: string[] = [];
|
||||||
const errors: Array<{ id: string; error: any }> = [];
|
const errors: Array<{ id: string; error: any }> = [];
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
|
||||||
if (isEsError(err)) {
|
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
return response.customError({ statusCode: 500, body: err });
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map((_id) =>
|
ids.map((_id) =>
|
||||||
context
|
client.asCurrentUser.ccr
|
||||||
.crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', {
|
.pauseAutoFollowPattern({
|
||||||
id: _id,
|
name: _id,
|
||||||
})
|
})
|
||||||
.then(() => itemsPaused.push(_id))
|
.then(() => itemsPaused.push(_id))
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
errors.push({ id: _id, error: formatError(err) });
|
errors.push({ id: _id, error: handleEsError({ error, response }) });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense, mockError } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerResumeRoute } from './register_resume_route';
|
import { registerResumeRoute } from './register_resume_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
registerResumeRoute({
|
registerResumeRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +33,9 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('resumes a single item', async () => {
|
it('resumes a single item', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
ccr: {
|
||||||
|
resumeAutoFollowPattern: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -51,11 +49,13 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('resumes multiple items', async () => {
|
it('resumes multiple items', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
resumeAutoFollowPattern: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -69,10 +69,12 @@ describe('[CCR API] Resume auto-follow pattern(s)', () => {
|
||||||
|
|
||||||
it('returns partial errors', async () => {
|
it('returns partial errors', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
resumeAutoFollowPattern: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockRejectedValueOnce({ response: { error: {} } }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockRejectedValueOnce(mockError),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerResumeRoute = ({
|
export const registerResumeRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -29,29 +29,22 @@ export const registerResumeRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const ids = id.split(',');
|
const ids = id.split(',');
|
||||||
|
|
||||||
const itemsResumed: string[] = [];
|
const itemsResumed: string[] = [];
|
||||||
const errors: Array<{ id: string; error: any }> = [];
|
const errors: Array<{ id: string; error: any }> = [];
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
|
||||||
if (isEsError(err)) {
|
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
return response.customError({ statusCode: 500, body: err });
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map((_id: string) =>
|
ids.map((_id: string) =>
|
||||||
context
|
client.asCurrentUser.ccr
|
||||||
.crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', {
|
.resumeAutoFollowPattern({
|
||||||
id: _id,
|
name: _id,
|
||||||
})
|
})
|
||||||
.then(() => itemsResumed.push(_id))
|
.then(() => itemsResumed.push(_id))
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
errors.push({ id: _id, error: formatError(err) });
|
errors.push({ id: _id, error: handleEsError({ error, response }) });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerUpdateRoute } from './register_update_route';
|
import { registerUpdateRoute } from './register_update_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Update auto-follow pattern', () => {
|
||||||
|
|
||||||
registerUpdateRoute({
|
registerUpdateRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,8 +33,10 @@ describe('[CCR API] Update auto-follow pattern', () => {
|
||||||
|
|
||||||
it('should serialize the payload before sending it to Elasticsearch', async () => {
|
it('should serialize the payload before sending it to Elasticsearch', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
// Just echo back what we send so we can inspect it.
|
ccr: {
|
||||||
callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload),
|
// Just echo back what we send so we can inspect it.
|
||||||
|
putAutoFollowPattern: jest.fn().mockImplementation((payload) => ({ body: payload })),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -53,7 +51,7 @@ describe('[CCR API] Update auto-follow pattern', () => {
|
||||||
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
|
const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
|
||||||
|
|
||||||
expect(response.payload).toEqual({
|
expect(response.payload).toEqual({
|
||||||
id: 'foo',
|
name: 'foo',
|
||||||
body: {
|
body: {
|
||||||
remote_cluster: 'bar1',
|
remote_cluster: 'bar1',
|
||||||
leader_index_patterns: ['bar2'],
|
leader_index_patterns: ['bar2'],
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerUpdateRoute = ({
|
export const registerUpdateRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -39,22 +39,21 @@ export const registerUpdateRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const body = serializeAutoFollowPattern(request.body as AutoFollowPattern);
|
const body = serializeAutoFollowPattern(request.body as AutoFollowPattern);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return response.ok({
|
const { body: responseBody } = await client.asCurrentUser.ccr.putAutoFollowPattern({
|
||||||
body: await context.crossClusterReplication!.client.callAsCurrentUser(
|
name: id,
|
||||||
'ccr.saveAutoFollowPattern',
|
body,
|
||||||
{ id, body }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
if (isEsError(err)) {
|
return response.ok({
|
||||||
return response.customError(formatEsError(err));
|
body: responseBody,
|
||||||
}
|
});
|
||||||
// Case: default
|
} catch (error) {
|
||||||
throw err;
|
return handleEsError({ error, response });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerPermissionsRoute = ({
|
export const registerPermissionsRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,8 @@ export const registerPermissionsRoute = ({
|
||||||
validate: false,
|
validate: false,
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
|
|
||||||
if (!license.isEsSecurityEnabled) {
|
if (!license.isEsSecurityEnabled) {
|
||||||
// If security has been disabled in elasticsearch.yml. we'll just let the user use CCR
|
// If security has been disabled in elasticsearch.yml. we'll just let the user use CCR
|
||||||
// because permissions are irrelevant.
|
// because permissions are irrelevant.
|
||||||
|
@ -35,9 +37,8 @@ export const registerPermissionsRoute = ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
has_all_requested: hasPermission,
|
body: { has_all_requested: hasPermission, cluster },
|
||||||
cluster,
|
} = await client.asCurrentUser.security.hasPrivileges({
|
||||||
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', {
|
|
||||||
body: {
|
body: {
|
||||||
cluster: ['manage', 'manage_ccr'],
|
cluster: ['manage', 'manage_ccr'],
|
||||||
},
|
},
|
||||||
|
@ -59,12 +60,8 @@ export const registerPermissionsRoute = ({
|
||||||
missingClusterPrivileges,
|
missingClusterPrivileges,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerStatsRoute = ({
|
export const registerStatsRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
|
@ -23,20 +23,19 @@ export const registerStatsRoute = ({
|
||||||
validate: false,
|
validate: false,
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
auto_follow_stats: autoFollowStats,
|
body: { auto_follow_stats: autoFollowStats },
|
||||||
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats');
|
} = await client.asCurrentUser.ccr.stats();
|
||||||
|
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
// @ts-expect-error Once #98266 is merged, test this again.
|
||||||
body: deserializeAutoFollowStats(autoFollowStats),
|
body: deserializeAutoFollowStats(autoFollowStats),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerCreateRoute } from './register_create_route';
|
import { registerCreateRoute } from './register_create_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Create follower index', () => {
|
||||||
|
|
||||||
registerCreateRoute({
|
registerCreateRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +33,9 @@ describe('[CCR API] Create follower index', () => {
|
||||||
|
|
||||||
it('should return 200 status when follower index is created', async () => {
|
it('should return 200 status when follower index is created', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
ccr: {
|
||||||
|
follow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerCreateRoute = ({
|
export const registerCreateRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const bodySchema = schema.object({
|
const bodySchema = schema.object({
|
||||||
name: schema.string(),
|
name: schema.string(),
|
||||||
|
@ -44,22 +44,21 @@ export const registerCreateRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { name, ...rest } = request.body;
|
const { name, ...rest } = request.body;
|
||||||
const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex));
|
const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return response.ok({
|
const { body: responseBody } = await client.asCurrentUser.ccr.follow({
|
||||||
body: await context.crossClusterReplication!.client.callAsCurrentUser(
|
index: name,
|
||||||
'ccr.saveFollowerIndex',
|
body,
|
||||||
{ name, body }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
if (isEsError(err)) {
|
return response.ok({
|
||||||
return response.customError(formatEsError(err));
|
body: responseBody,
|
||||||
}
|
});
|
||||||
// Case: default
|
} catch (error) {
|
||||||
throw err;
|
return handleEsError({ error, response });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerFetchRoute } from './register_fetch_route';
|
import { registerFetchRoute } from './register_fetch_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Fetch all follower indices', () => {
|
||||||
|
|
||||||
registerFetchRoute({
|
registerFetchRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,73 +33,77 @@ describe('[CCR API] Fetch all follower indices', () => {
|
||||||
|
|
||||||
it('deserializes the response from Elasticsearch', async () => {
|
it('deserializes the response from Elasticsearch', async () => {
|
||||||
const ccrInfoMockResponse = {
|
const ccrInfoMockResponse = {
|
||||||
follower_indices: [
|
body: {
|
||||||
{
|
follower_indices: [
|
||||||
follower_index: 'followerIndexName',
|
|
||||||
remote_cluster: 'remoteCluster',
|
|
||||||
leader_index: 'leaderIndex',
|
|
||||||
status: 'active',
|
|
||||||
parameters: {
|
|
||||||
max_read_request_operation_count: 1,
|
|
||||||
max_outstanding_read_requests: 1,
|
|
||||||
max_read_request_size: '1b',
|
|
||||||
max_write_request_operation_count: 1,
|
|
||||||
max_write_request_size: '1b',
|
|
||||||
max_outstanding_write_requests: 1,
|
|
||||||
max_write_buffer_count: 1,
|
|
||||||
max_write_buffer_size: '1b',
|
|
||||||
max_retry_delay: '1s',
|
|
||||||
read_poll_timeout: '1s',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// These stats correlate to the above follower indices.
|
|
||||||
const ccrStatsMockResponse = {
|
|
||||||
follow_stats: {
|
|
||||||
indices: [
|
|
||||||
{
|
{
|
||||||
index: 'followerIndexName',
|
follower_index: 'followerIndexName',
|
||||||
shards: [
|
remote_cluster: 'remoteCluster',
|
||||||
{
|
leader_index: 'leaderIndex',
|
||||||
shard_id: 1,
|
status: 'active',
|
||||||
leader_index: 'leaderIndex',
|
parameters: {
|
||||||
leader_global_checkpoint: 1,
|
max_read_request_operation_count: 1,
|
||||||
leader_max_seq_no: 1,
|
max_outstanding_read_requests: 1,
|
||||||
follower_global_checkpoint: 1,
|
max_read_request_size: '1b',
|
||||||
follower_max_seq_no: 1,
|
max_write_request_operation_count: 1,
|
||||||
last_requested_seq_no: 1,
|
max_write_request_size: '1b',
|
||||||
outstanding_read_requests: 1,
|
max_outstanding_write_requests: 1,
|
||||||
outstanding_write_requests: 1,
|
max_write_buffer_count: 1,
|
||||||
write_buffer_operation_count: 1,
|
max_write_buffer_size: '1b',
|
||||||
write_buffer_size_in_bytes: 1,
|
max_retry_delay: '1s',
|
||||||
follower_mapping_version: 1,
|
read_poll_timeout: '1s',
|
||||||
follower_settings_version: 1,
|
},
|
||||||
total_read_time_millis: 1,
|
|
||||||
total_read_remote_exec_time_millis: 1,
|
|
||||||
successful_read_requests: 1,
|
|
||||||
failed_read_requests: 1,
|
|
||||||
operations_read: 1,
|
|
||||||
bytes_read: 1,
|
|
||||||
total_write_time_millis: 1,
|
|
||||||
successful_write_requests: 1,
|
|
||||||
failed_write_requests: 1,
|
|
||||||
operations_written: 1,
|
|
||||||
read_exceptions: 1,
|
|
||||||
time_since_last_read_millis: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// These stats correlate to the above follower indices.
|
||||||
|
const ccrStatsMockResponse = {
|
||||||
|
body: {
|
||||||
|
follow_stats: {
|
||||||
|
indices: [
|
||||||
|
{
|
||||||
|
index: 'followerIndexName',
|
||||||
|
shards: [
|
||||||
|
{
|
||||||
|
shard_id: 1,
|
||||||
|
leader_index: 'leaderIndex',
|
||||||
|
leader_global_checkpoint: 1,
|
||||||
|
leader_max_seq_no: 1,
|
||||||
|
follower_global_checkpoint: 1,
|
||||||
|
follower_max_seq_no: 1,
|
||||||
|
last_requested_seq_no: 1,
|
||||||
|
outstanding_read_requests: 1,
|
||||||
|
outstanding_write_requests: 1,
|
||||||
|
write_buffer_operation_count: 1,
|
||||||
|
write_buffer_size_in_bytes: 1,
|
||||||
|
follower_mapping_version: 1,
|
||||||
|
follower_settings_version: 1,
|
||||||
|
total_read_time_millis: 1,
|
||||||
|
total_read_remote_exec_time_millis: 1,
|
||||||
|
successful_read_requests: 1,
|
||||||
|
failed_read_requests: 1,
|
||||||
|
operations_read: 1,
|
||||||
|
bytes_read: 1,
|
||||||
|
total_write_time_millis: 1,
|
||||||
|
successful_write_requests: 1,
|
||||||
|
failed_write_requests: 1,
|
||||||
|
operations_written: 1,
|
||||||
|
read_exceptions: 1,
|
||||||
|
time_since_last_read_millis: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
followInfo: jest.fn().mockResolvedValueOnce(ccrInfoMockResponse),
|
||||||
.mockResolvedValueOnce(ccrInfoMockResponse)
|
stats: jest.fn().mockResolvedValueOnce(ccrStatsMockResponse),
|
||||||
.mockResolvedValueOnce(ccrStatsMockResponse),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest();
|
const request = httpServerMock.createKibanaRequest();
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerFetchRoute = ({
|
export const registerFetchRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
|
@ -23,16 +23,18 @@ export const registerFetchRoute = ({
|
||||||
validate: false,
|
validate: false,
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
follower_indices: followerIndices,
|
body: { follower_indices: followerIndices },
|
||||||
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', {
|
} = await client.asCurrentUser.ccr.followInfo({ index: '_all' });
|
||||||
id: '_all',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
follow_stats: { indices: followerIndicesStats },
|
body: {
|
||||||
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats');
|
follow_stats: { indices: followerIndicesStats },
|
||||||
|
},
|
||||||
|
} = await client.asCurrentUser.ccr.stats();
|
||||||
|
|
||||||
const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
|
const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
|
||||||
map[stats.index] = stats;
|
map[stats.index] = stats;
|
||||||
|
@ -51,12 +53,8 @@ export const registerFetchRoute = ({
|
||||||
indices: deserializeListFollowerIndices(collatedFollowerIndices),
|
indices: deserializeListFollowerIndices(collatedFollowerIndices),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerGetRoute } from './register_get_route';
|
import { registerGetRoute } from './register_get_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Get one follower index', () => {
|
||||||
|
|
||||||
registerGetRoute({
|
registerGetRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,71 +33,75 @@ describe('[CCR API] Get one follower index', () => {
|
||||||
|
|
||||||
it('should return a single resource even though ES returns an array with 1 item', async () => {
|
it('should return a single resource even though ES returns an array with 1 item', async () => {
|
||||||
const ccrInfoMockResponse = {
|
const ccrInfoMockResponse = {
|
||||||
follower_indices: [
|
body: {
|
||||||
{
|
follower_indices: [
|
||||||
follower_index: 'followerIndexName',
|
{
|
||||||
remote_cluster: 'remoteCluster',
|
follower_index: 'followerIndexName',
|
||||||
leader_index: 'leaderIndex',
|
remote_cluster: 'remoteCluster',
|
||||||
status: 'active',
|
leader_index: 'leaderIndex',
|
||||||
parameters: {
|
status: 'active',
|
||||||
max_read_request_operation_count: 1,
|
parameters: {
|
||||||
max_outstanding_read_requests: 1,
|
max_read_request_operation_count: 1,
|
||||||
max_read_request_size: '1b',
|
max_outstanding_read_requests: 1,
|
||||||
max_write_request_operation_count: 1,
|
max_read_request_size: '1b',
|
||||||
max_write_request_size: '1b',
|
max_write_request_operation_count: 1,
|
||||||
max_outstanding_write_requests: 1,
|
max_write_request_size: '1b',
|
||||||
max_write_buffer_count: 1,
|
max_outstanding_write_requests: 1,
|
||||||
max_write_buffer_size: '1b',
|
max_write_buffer_count: 1,
|
||||||
max_retry_delay: '1s',
|
max_write_buffer_size: '1b',
|
||||||
read_poll_timeout: '1s',
|
max_retry_delay: '1s',
|
||||||
|
read_poll_timeout: '1s',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// These stats correlate to the above follower indices.
|
// These stats correlate to the above follower indices.
|
||||||
const ccrFollowerIndexStatsMockResponse = {
|
const ccrFollowerIndexStatsMockResponse = {
|
||||||
indices: [
|
body: {
|
||||||
{
|
indices: [
|
||||||
index: 'followerIndexName',
|
{
|
||||||
shards: [
|
index: 'followerIndexName',
|
||||||
{
|
shards: [
|
||||||
shard_id: 1,
|
{
|
||||||
leader_index: 'leaderIndex',
|
shard_id: 1,
|
||||||
leader_global_checkpoint: 1,
|
leader_index: 'leaderIndex',
|
||||||
leader_max_seq_no: 1,
|
leader_global_checkpoint: 1,
|
||||||
follower_global_checkpoint: 1,
|
leader_max_seq_no: 1,
|
||||||
follower_max_seq_no: 1,
|
follower_global_checkpoint: 1,
|
||||||
last_requested_seq_no: 1,
|
follower_max_seq_no: 1,
|
||||||
outstanding_read_requests: 1,
|
last_requested_seq_no: 1,
|
||||||
outstanding_write_requests: 1,
|
outstanding_read_requests: 1,
|
||||||
write_buffer_operation_count: 1,
|
outstanding_write_requests: 1,
|
||||||
write_buffer_size_in_bytes: 1,
|
write_buffer_operation_count: 1,
|
||||||
follower_mapping_version: 1,
|
write_buffer_size_in_bytes: 1,
|
||||||
follower_settings_version: 1,
|
follower_mapping_version: 1,
|
||||||
total_read_time_millis: 1,
|
follower_settings_version: 1,
|
||||||
total_read_remote_exec_time_millis: 1,
|
total_read_time_millis: 1,
|
||||||
successful_read_requests: 1,
|
total_read_remote_exec_time_millis: 1,
|
||||||
failed_read_requests: 1,
|
successful_read_requests: 1,
|
||||||
operations_read: 1,
|
failed_read_requests: 1,
|
||||||
bytes_read: 1,
|
operations_read: 1,
|
||||||
total_write_time_millis: 1,
|
bytes_read: 1,
|
||||||
successful_write_requests: 1,
|
total_write_time_millis: 1,
|
||||||
failed_write_requests: 1,
|
successful_write_requests: 1,
|
||||||
operations_written: 1,
|
failed_write_requests: 1,
|
||||||
read_exceptions: 1,
|
operations_written: 1,
|
||||||
time_since_last_read_millis: 1,
|
read_exceptions: 1,
|
||||||
},
|
time_since_last_read_millis: 1,
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
followInfo: jest.fn().mockResolvedValueOnce(ccrInfoMockResponse),
|
||||||
.mockResolvedValueOnce(ccrInfoMockResponse)
|
followStats: jest.fn().mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse),
|
||||||
.mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerGetRoute = ({
|
export const registerGetRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
id: schema.string(),
|
id: schema.string(),
|
||||||
|
@ -30,12 +30,13 @@ export const registerGetRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
follower_indices: followerIndices,
|
body: { follower_indices: followerIndices },
|
||||||
} = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id });
|
} = await client.asCurrentUser.ccr.followInfo({ index: id });
|
||||||
|
|
||||||
const followerIndexInfo = followerIndices && followerIndices[0];
|
const followerIndexInfo = followerIndices && followerIndices[0];
|
||||||
|
|
||||||
|
@ -48,31 +49,26 @@ export const registerGetRoute = ({
|
||||||
// If this follower is paused, skip call to ES stats api since it will return 404
|
// If this follower is paused, skip call to ES stats api since it will return 404
|
||||||
if (followerIndexInfo.status === 'paused') {
|
if (followerIndexInfo.status === 'paused') {
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
// @ts-expect-error Once #98266 is merged, test this again.
|
||||||
body: deserializeFollowerIndex({
|
body: deserializeFollowerIndex({
|
||||||
...followerIndexInfo,
|
...followerIndexInfo,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const {
|
const {
|
||||||
indices: followerIndicesStats,
|
body: { indices: followerIndicesStats },
|
||||||
} = await context.crossClusterReplication!.client.callAsCurrentUser(
|
} = await client.asCurrentUser.ccr.followStats({ index: id });
|
||||||
'ccr.followerIndexStats',
|
|
||||||
{ id }
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
// @ts-expect-error Once #98266 is merged, test this again.
|
||||||
body: deserializeFollowerIndex({
|
body: deserializeFollowerIndex({
|
||||||
...followerIndexInfo,
|
...followerIndexInfo,
|
||||||
...(followerIndicesStats ? followerIndicesStats[0] : {}),
|
...(followerIndicesStats ? followerIndicesStats[0] : {}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (isEsError(err)) {
|
return handleEsError({ error, response });
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense, mockError } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerPauseRoute } from './register_pause_route';
|
import { registerPauseRoute } from './register_pause_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Pause follower index/indices', () => {
|
||||||
|
|
||||||
registerPauseRoute({
|
registerPauseRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +33,9 @@ describe('[CCR API] Pause follower index/indices', () => {
|
||||||
|
|
||||||
it('pauses a single item', async () => {
|
it('pauses a single item', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
ccr: {
|
||||||
|
pauseFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -51,11 +49,13 @@ describe('[CCR API] Pause follower index/indices', () => {
|
||||||
|
|
||||||
it('pauses multiple items', async () => {
|
it('pauses multiple items', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseFollow: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -69,10 +69,12 @@ describe('[CCR API] Pause follower index/indices', () => {
|
||||||
|
|
||||||
it('returns partial errors', async () => {
|
it('returns partial errors', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseFollow: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockRejectedValueOnce({ response: { error: {} } }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockRejectedValueOnce(mockError),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerPauseRoute = ({
|
export const registerPauseRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({ id: schema.string() });
|
const paramsSchema = schema.object({ id: schema.string() });
|
||||||
|
|
||||||
|
@ -27,29 +27,20 @@ export const registerPauseRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const ids = id.split(',');
|
const ids = id.split(',');
|
||||||
|
|
||||||
const itemsPaused: string[] = [];
|
const itemsPaused: string[] = [];
|
||||||
const errors: Array<{ id: string; error: any }> = [];
|
const errors: Array<{ id: string; error: any }> = [];
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
|
||||||
if (isEsError(err)) {
|
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
return response.customError({ statusCode: 500, body: err });
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map((_id: string) =>
|
ids.map((_id: string) =>
|
||||||
context
|
client.asCurrentUser.ccr
|
||||||
.crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', {
|
.pauseFollow({ index: _id })
|
||||||
id: _id,
|
|
||||||
})
|
|
||||||
.then(() => itemsPaused.push(_id))
|
.then(() => itemsPaused.push(_id))
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
errors.push({ id: _id, error: formatError(err) });
|
errors.push({ id: _id, error: handleEsError({ error, response }) });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense, mockError } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerResumeRoute } from './register_resume_route';
|
import { registerResumeRoute } from './register_resume_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Resume follower index/indices', () => {
|
||||||
|
|
||||||
registerResumeRoute({
|
registerResumeRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +33,9 @@ describe('[CCR API] Resume follower index/indices', () => {
|
||||||
|
|
||||||
it('resumes a single item', async () => {
|
it('resumes a single item', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
ccr: {
|
||||||
|
resumeFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -51,11 +49,13 @@ describe('[CCR API] Resume follower index/indices', () => {
|
||||||
|
|
||||||
it('resumes multiple items', async () => {
|
it('resumes multiple items', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
resumeFollow: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -69,10 +69,12 @@ describe('[CCR API] Resume follower index/indices', () => {
|
||||||
|
|
||||||
it('returns partial errors', async () => {
|
it('returns partial errors', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
resumeFollow: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockRejectedValueOnce({ response: { error: {} } }),
|
.mockResolvedValueOnce({ acknowledge: true })
|
||||||
|
.mockRejectedValueOnce(mockError),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerResumeRoute = ({
|
export const registerResumeRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({ id: schema.string() });
|
const paramsSchema = schema.object({ id: schema.string() });
|
||||||
|
|
||||||
|
@ -27,29 +27,20 @@ export const registerResumeRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const ids = id.split(',');
|
const ids = id.split(',');
|
||||||
|
|
||||||
const itemsResumed: string[] = [];
|
const itemsResumed: string[] = [];
|
||||||
const errors: Array<{ id: string; error: any }> = [];
|
const errors: Array<{ id: string; error: any }> = [];
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
|
||||||
if (isEsError(err)) {
|
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
return response.customError({ statusCode: 500, body: err });
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map((_id: string) =>
|
ids.map((_id: string) =>
|
||||||
context
|
client.asCurrentUser.ccr
|
||||||
.crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', {
|
.resumeFollow({ index: _id })
|
||||||
id: _id,
|
|
||||||
})
|
|
||||||
.then(() => itemsResumed.push(_id))
|
.then(() => itemsResumed.push(_id))
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
errors.push({ id: _id, error: formatError(err) });
|
errors.push({ id: _id, error: handleEsError({ error, response }) });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
|
||||||
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
|
||||||
|
|
||||||
import { isEsError, License } from '../../../shared_imports';
|
import { handleEsError } from '../../../shared_imports';
|
||||||
import { formatEsError } from '../../../lib/format_es_error';
|
import { mockRouteContext, mockLicense, mockError } from '../test_lib';
|
||||||
import { mockRouteContext } from '../test_lib';
|
|
||||||
import { registerUnfollowRoute } from './register_unfollow_route';
|
import { registerUnfollowRoute } from './register_unfollow_route';
|
||||||
|
|
||||||
const httpService = httpServiceMock.createSetupContract();
|
const httpService = httpServiceMock.createSetupContract();
|
||||||
|
@ -23,12 +22,9 @@ describe('[CCR API] Unfollow follower index/indices', () => {
|
||||||
|
|
||||||
registerUnfollowRoute({
|
registerUnfollowRoute({
|
||||||
router,
|
router,
|
||||||
license: {
|
license: mockLicense,
|
||||||
guardApiRoute: (route: any) => route,
|
|
||||||
} as License,
|
|
||||||
lib: {
|
lib: {
|
||||||
isEsError,
|
handleEsError,
|
||||||
formatEsError,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,12 +33,14 @@ describe('[CCR API] Unfollow follower index/indices', () => {
|
||||||
|
|
||||||
it('unfollows a single item', async () => {
|
it('unfollows a single item', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseFollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
unfollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
},
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
indices: {
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
close: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
open: jest.fn().mockResolvedValueOnce({ acknowledge: true }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -56,23 +54,30 @@ describe('[CCR API] Unfollow follower index/indices', () => {
|
||||||
|
|
||||||
it('unfollows multiple items', async () => {
|
it('unfollows multiple items', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseFollow: jest
|
||||||
// a
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }) // a
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }) // b
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }), // c
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
unfollow: jest
|
||||||
// b
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }) // a
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }) // b
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }), // c
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
},
|
||||||
// c
|
indices: {
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
close: jest
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }) // a
|
||||||
.mockResolvedValueOnce({ acknowledge: true }),
|
.mockResolvedValueOnce({ acknowledge: true }) // b
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }), // c
|
||||||
|
open: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }) // a
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }) // b
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }), // c
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
@ -86,16 +91,20 @@ describe('[CCR API] Unfollow follower index/indices', () => {
|
||||||
|
|
||||||
it('returns partial errors', async () => {
|
it('returns partial errors', async () => {
|
||||||
const routeContextMock = mockRouteContext({
|
const routeContextMock = mockRouteContext({
|
||||||
callAsCurrentUser: jest
|
ccr: {
|
||||||
.fn()
|
pauseFollow: jest
|
||||||
// a
|
.fn()
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }) // a
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
.mockResolvedValueOnce({ acknowledge: true }), // B
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
unfollow: jest.fn().mockResolvedValueOnce({ acknowledge: true }), // a
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
},
|
||||||
// b
|
indices: {
|
||||||
.mockResolvedValueOnce({ acknowledge: true })
|
close: jest
|
||||||
.mockRejectedValueOnce({ response: { error: {} } }),
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ acknowledge: true }) // a
|
||||||
|
.mockRejectedValueOnce(mockError), // b
|
||||||
|
open: jest.fn().mockResolvedValueOnce({ acknowledge: true }), // a
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = httpServerMock.createKibanaRequest({
|
const request = httpServerMock.createKibanaRequest({
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RouteDependencies } from '../../../types';
|
||||||
export const registerUnfollowRoute = ({
|
export const registerUnfollowRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({ id: schema.string() });
|
const paramsSchema = schema.object({ id: schema.string() });
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ export const registerUnfollowRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const ids = id.split(',');
|
const ids = id.split(',');
|
||||||
|
|
||||||
|
@ -34,52 +35,34 @@ export const registerUnfollowRoute = ({
|
||||||
const itemsNotOpen: string[] = [];
|
const itemsNotOpen: string[] = [];
|
||||||
const errors: Array<{ id: string; error: any }> = [];
|
const errors: Array<{ id: string; error: any }> = [];
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
|
||||||
if (isEsError(err)) {
|
|
||||||
return response.customError(formatEsError(err));
|
|
||||||
}
|
|
||||||
// Case: default
|
|
||||||
return response.customError({ statusCode: 500, body: err });
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map(async (_id: string) => {
|
ids.map(async (_id: string) => {
|
||||||
try {
|
try {
|
||||||
// Try to pause follower, let it fail silently since it may already be paused
|
// Try to pause follower, let it fail silently since it may already be paused
|
||||||
try {
|
try {
|
||||||
await context.crossClusterReplication!.client.callAsCurrentUser(
|
await client.asCurrentUser.ccr.pauseFollow({ index: _id });
|
||||||
'ccr.pauseFollowerIndex',
|
|
||||||
{ id: _id }
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Swallow errors
|
// Swallow errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close index
|
// Close index
|
||||||
await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', {
|
await client.asCurrentUser.indices.close({ index: _id });
|
||||||
index: _id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unfollow leader
|
// Unfollow leader
|
||||||
await context.crossClusterReplication!.client.callAsCurrentUser(
|
await client.asCurrentUser.ccr.unfollow({ index: _id });
|
||||||
'ccr.unfollowLeaderIndex',
|
|
||||||
{ id: _id }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Try to re-open the index, store failures in a separate array to surface warnings in the UI
|
// Try to re-open the index, store failures in a separate array to surface warnings in the UI
|
||||||
// This will allow users to query their index normally after unfollowing
|
// This will allow users to query their index normally after unfollowing
|
||||||
try {
|
try {
|
||||||
await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', {
|
await client.asCurrentUser.indices.open({ index: _id });
|
||||||
index: _id,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
itemsNotOpen.push(_id);
|
itemsNotOpen.push(_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push success
|
// Push success
|
||||||
itemsUnfollowed.push(_id);
|
itemsUnfollowed.push(_id);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
errors.push({ id: _id, error: formatError(err) });
|
errors.push({ id: _id, error: handleEsError({ error, response }) });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 = ({
|
export const registerUpdateRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
lib: { isEsError, formatEsError },
|
lib: { handleEsError },
|
||||||
}: RouteDependencies) => {
|
}: RouteDependencies) => {
|
||||||
const paramsSchema = schema.object({ id: schema.string() });
|
const paramsSchema = schema.object({ id: schema.string() });
|
||||||
|
|
||||||
|
@ -44,13 +44,14 @@ export const registerUpdateRoute = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (context, request, response) => {
|
license.guardApiRoute(async (context, request, response) => {
|
||||||
|
const { client } = context.core.elasticsearch;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
|
||||||
// We need to first pause the follower and then resume it by passing the advanced settings
|
// We need to first pause the follower and then resume it by passing the advanced settings
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
follower_indices: followerIndices,
|
body: { follower_indices: followerIndices },
|
||||||
} = await context.crossClusterReplication.client.callAsCurrentUser('ccr.info', { id });
|
} = await client.asCurrentUser.ccr.followInfo({ index: id });
|
||||||
|
|
||||||
const followerIndexInfo = followerIndices && followerIndices[0];
|
const followerIndexInfo = followerIndices && followerIndices[0];
|
||||||
|
|
||||||
|
@ -63,12 +64,7 @@ export const registerUpdateRoute = ({
|
||||||
|
|
||||||
// Pause follower if not already paused
|
// Pause follower if not already paused
|
||||||
if (!isPaused) {
|
if (!isPaused) {
|
||||||
await context.crossClusterReplication!.client.callAsCurrentUser(
|
await client.asCurrentUser.ccr.pauseFollow({ index: id });
|
||||||
'ccr.pauseFollowerIndex',
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume follower
|
// Resume follower
|
||||||
|
@ -76,18 +72,16 @@ export const registerUpdateRoute = ({
|
||||||
serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings)
|
serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings)
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.ok({
|
const { body: responseBody } = await client.asCurrentUser.ccr.resumeFollow({
|
||||||
body: await context.crossClusterReplication!.client.callAsCurrentUser(
|
index: id,
|
||||||
'ccr.resumeFollowerIndex',
|
body,
|
||||||
{ id, body }
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
if (isEsError(err)) {
|
return response.ok({
|
||||||
return response.customError(formatEsError(err));
|
body: responseBody,
|
||||||
}
|
});
|
||||||
// Case: default
|
} catch (error) {
|
||||||
throw err;
|
return handleEsError({ error, response });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,19 +6,24 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RequestHandlerContext } from 'src/core/server';
|
import { RequestHandlerContext } from 'src/core/server';
|
||||||
|
import { License } from '../../shared_imports';
|
||||||
|
|
||||||
export function mockRouteContext({
|
export function mockRouteContext(mockedFunctions: unknown): RequestHandlerContext {
|
||||||
callAsCurrentUser,
|
|
||||||
}: {
|
|
||||||
callAsCurrentUser: any;
|
|
||||||
}): RequestHandlerContext {
|
|
||||||
const routeContextMock = ({
|
const routeContextMock = ({
|
||||||
crossClusterReplication: {
|
core: {
|
||||||
client: {
|
elasticsearch: {
|
||||||
callAsCurrentUser,
|
client: {
|
||||||
|
asCurrentUser: mockedFunctions,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown) as RequestHandlerContext;
|
} as unknown) as RequestHandlerContext;
|
||||||
|
|
||||||
return routeContextMock;
|
return routeContextMock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mockLicense = {
|
||||||
|
guardApiRoute: (route: any) => route,
|
||||||
|
} as License;
|
||||||
|
|
||||||
|
export const mockError = { name: 'ResponseError', statusCode: 400 };
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
|
export { handleEsError } from '../../../../src/plugins/es_ui_shared/server';
|
||||||
export { License } from '../../license_api_guard/server';
|
export { License } from '../../license_api_guard/server';
|
||||||
|
|
|
@ -5,13 +5,12 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server';
|
import { IRouter } from 'src/core/server';
|
||||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||||
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
|
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
|
||||||
import { IndexManagementPluginSetup } from '../../index_management/server';
|
import { IndexManagementPluginSetup } from '../../index_management/server';
|
||||||
import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
|
import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
|
||||||
import { License, isEsError } from './shared_imports';
|
import { License, handleEsError } from './shared_imports';
|
||||||
import { formatEsError } from './lib/format_es_error';
|
|
||||||
|
|
||||||
export interface SetupDependencies {
|
export interface SetupDependencies {
|
||||||
licensing: LicensingPluginSetup;
|
licensing: LicensingPluginSetup;
|
||||||
|
@ -25,24 +24,9 @@ export interface StartDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteDependencies {
|
export interface RouteDependencies {
|
||||||
router: CcrPluginRouter;
|
router: IRouter;
|
||||||
license: License;
|
license: License;
|
||||||
lib: {
|
lib: {
|
||||||
isEsError: typeof isEsError;
|
handleEsError: typeof handleEsError;
|
||||||
formatEsError: typeof formatEsError;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export interface CcrRequestHandlerContext extends RequestHandlerContext {
|
|
||||||
crossClusterReplication: {
|
|
||||||
client: ILegacyScopedClusterClient;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
type CcrPluginRouter = IRouter<CcrRequestHandlerContext>;
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default function ({ getService }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(body.statusCode).to.be(404);
|
expect(body.statusCode).to.be(404);
|
||||||
expect(body.attributes.cause[0]).to.contain('no such remote cluster');
|
expect(body.attributes.error.reason).to.contain('no such remote cluster');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ export default function ({ getService }) {
|
||||||
it('should return a 404 when the auto-follow pattern is not found', async () => {
|
it('should return a 404 when the auto-follow pattern is not found', async () => {
|
||||||
const { body } = await getAutoFollowPattern('missing-pattern');
|
const { body } = await getAutoFollowPattern('missing-pattern');
|
||||||
expect(body.statusCode).to.be(404);
|
expect(body.statusCode).to.be(404);
|
||||||
expect(body.attributes.cause).not.to.be(undefined);
|
expect(body.attributes.error.reason).not.to.be(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an auto-follow pattern that was created', async () => {
|
it('should return an auto-follow pattern that was created', async () => {
|
||||||
|
|
|
@ -6,10 +6,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { REMOTE_CLUSTER_NAME } from './constants';
|
import { REMOTE_CLUSTER_NAME } from './constants';
|
||||||
import { getRandomString } from './lib';
|
|
||||||
|
|
||||||
export const getFollowerIndexPayload = (
|
export const getFollowerIndexPayload = (
|
||||||
leaderIndexName = getRandomString(),
|
leaderIndexName = 'test-leader-index',
|
||||||
remoteCluster = REMOTE_CLUSTER_NAME,
|
remoteCluster = REMOTE_CLUSTER_NAME,
|
||||||
advancedSettings = {}
|
advancedSettings = {}
|
||||||
) => ({
|
) => ({
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { API_BASE_PATH } from './constants';
|
import { API_BASE_PATH } from './constants';
|
||||||
import { getRandomString } from './lib';
|
|
||||||
import { getFollowerIndexPayload } from './fixtures';
|
import { getFollowerIndexPayload } from './fixtures';
|
||||||
|
|
||||||
export const registerHelpers = (supertest) => {
|
export const registerHelpers = (supertest) => {
|
||||||
|
@ -51,7 +50,7 @@ export const registerHelpers = (supertest) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFollowerIndex = (name = getRandomString(), payload = getFollowerIndexPayload()) => {
|
const createFollowerIndex = (name, payload = getFollowerIndexPayload()) => {
|
||||||
followerIndicesCreated.push(name);
|
followerIndicesCreated.push(name);
|
||||||
|
|
||||||
return supertest
|
return supertest
|
||||||
|
|
|
@ -9,7 +9,7 @@ import expect from '@kbn/expect';
|
||||||
|
|
||||||
import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants';
|
import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants';
|
||||||
import { getFollowerIndexPayload } from './fixtures';
|
import { getFollowerIndexPayload } from './fixtures';
|
||||||
import { registerHelpers as registerElasticSearchHelpers, getRandomString } from './lib';
|
import { registerHelpers as registerElasticSearchHelpers } from './lib';
|
||||||
import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers';
|
import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers';
|
||||||
import { registerHelpers as registerFollowerIndicesnHelpers } from './follower_indices.helpers';
|
import { registerHelpers as registerFollowerIndicesnHelpers } from './follower_indices.helpers';
|
||||||
|
|
||||||
|
@ -47,23 +47,23 @@ export default function ({ getService }) {
|
||||||
const payload = getFollowerIndexPayload();
|
const payload = getFollowerIndexPayload();
|
||||||
payload.remoteCluster = 'unknown-cluster';
|
payload.remoteCluster = 'unknown-cluster';
|
||||||
|
|
||||||
const { body } = await createFollowerIndex(undefined, payload).expect(404);
|
const { body } = await createFollowerIndex('test', payload).expect(404);
|
||||||
expect(body.attributes.cause[0]).to.contain('no such remote cluster');
|
expect(body.attributes.error.reason).to.contain('no such remote cluster');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw a 404 error trying to follow an unknown index', async () => {
|
it('should throw a 404 error trying to follow an unknown index', async () => {
|
||||||
const payload = getFollowerIndexPayload();
|
const payload = getFollowerIndexPayload();
|
||||||
const { body } = await createFollowerIndex(undefined, payload).expect(404);
|
const { body } = await createFollowerIndex('test', payload).expect(404);
|
||||||
expect(body.attributes.cause[0]).to.contain('no such index');
|
expect(body.attributes.error.reason).to.contain('no such index');
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: If this test fails locally it's probably because you have another cluster running.
|
// NOTE: If this test fails locally it's probably because you have another cluster running.
|
||||||
it('should create a follower index that follows an existing leader index', async () => {
|
it('should create a follower index that follows an existing leader index', async () => {
|
||||||
// First let's create an index to follow
|
// First let's create an index to follow
|
||||||
const leaderIndex = await createIndex();
|
const leaderIndex = await createIndex('leader1');
|
||||||
|
|
||||||
const payload = getFollowerIndexPayload(leaderIndex);
|
const payload = getFollowerIndexPayload(leaderIndex);
|
||||||
const { body } = await createFollowerIndex(undefined, payload).expect(200);
|
const { body } = await createFollowerIndex('index1', payload).expect(200);
|
||||||
|
|
||||||
// There is a race condition in which Elasticsearch can respond without acknowledging,
|
// There is a race condition in which Elasticsearch can respond without acknowledging,
|
||||||
// i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false.
|
// i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false.
|
||||||
|
@ -74,17 +74,17 @@ export default function ({ getService }) {
|
||||||
|
|
||||||
describe('get()', () => {
|
describe('get()', () => {
|
||||||
it('should return a 404 when the follower index does not exist', async () => {
|
it('should return a 404 when the follower index does not exist', async () => {
|
||||||
const name = getRandomString();
|
const name = 'test';
|
||||||
const { body } = await getFollowerIndex(name).expect(404);
|
const { body } = await getFollowerIndex(name).expect(404);
|
||||||
|
|
||||||
expect(body.attributes.cause[0]).to.contain('no such index');
|
expect(body.attributes.error.reason).to.contain('no such index');
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: If this test fails locally it's probably because you have another cluster running.
|
// NOTE: If this test fails locally it's probably because you have another cluster running.
|
||||||
it('should return a follower index that was created', async () => {
|
it('should return a follower index that was created', async () => {
|
||||||
const leaderIndex = await createIndex();
|
const leaderIndex = await createIndex('leader2');
|
||||||
|
|
||||||
const name = getRandomString();
|
const name = 'index2';
|
||||||
const payload = getFollowerIndexPayload(leaderIndex);
|
const payload = getFollowerIndexPayload(leaderIndex);
|
||||||
await createFollowerIndex(name, payload);
|
await createFollowerIndex(name, payload);
|
||||||
|
|
||||||
|
@ -98,8 +98,8 @@ export default function ({ getService }) {
|
||||||
describe('update()', () => {
|
describe('update()', () => {
|
||||||
it('should update a follower index advanced settings', async () => {
|
it('should update a follower index advanced settings', async () => {
|
||||||
// Create a follower index
|
// Create a follower index
|
||||||
const leaderIndex = await createIndex();
|
const leaderIndex = await createIndex('leader3');
|
||||||
const followerIndex = getRandomString();
|
const followerIndex = 'index3';
|
||||||
const initialValue = 1234;
|
const initialValue = 1234;
|
||||||
const payload = getFollowerIndexPayload(leaderIndex, undefined, {
|
const payload = getFollowerIndexPayload(leaderIndex, undefined, {
|
||||||
maxReadRequestOperationCount: initialValue,
|
maxReadRequestOperationCount: initialValue,
|
||||||
|
@ -128,9 +128,8 @@ export default function ({ getService }) {
|
||||||
* When we then retrieve the follower index it will have all the advanced settings
|
* When we then retrieve the follower index it will have all the advanced settings
|
||||||
* coming from ES. We can then compare those settings with our hard-coded values.
|
* coming from ES. We can then compare those settings with our hard-coded values.
|
||||||
*/
|
*/
|
||||||
const leaderIndex = await createIndex();
|
const leaderIndex = await createIndex('leader4');
|
||||||
|
const name = 'index4';
|
||||||
const name = getRandomString();
|
|
||||||
const payload = getFollowerIndexPayload(leaderIndex);
|
const payload = getFollowerIndexPayload(leaderIndex);
|
||||||
await createFollowerIndex(name, payload);
|
await createFollowerIndex(name, payload);
|
||||||
|
|
||||||
|
|
|
@ -5,20 +5,18 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getRandomString } from './random';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers to create and delete indices on the Elasticsearch instance
|
* Helpers to create and delete indices on the Elasticsearch instance
|
||||||
* during our tests.
|
* during our tests.
|
||||||
* @param {ElasticsearchClient} es The Elasticsearch client instance
|
* @param {ElasticsearchClient} es The Elasticsearch client instance
|
||||||
*/
|
*/
|
||||||
export const registerHelpers = (getService) => {
|
export const registerHelpers = (getService) => {
|
||||||
const es = getService('legacyEs');
|
const es = getService('es');
|
||||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||||
|
|
||||||
let indicesCreated = [];
|
let indicesCreated = [];
|
||||||
|
|
||||||
const createIndex = (index = getRandomString()) => {
|
const createIndex = (index) => {
|
||||||
indicesCreated.push(index);
|
indicesCreated.push(index);
|
||||||
return es.indices.create({ index }).then(() => index);
|
return es.indices.create({ index }).then(() => index);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,5 +6,3 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { registerHelpers } from './elasticsearch';
|
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