[Snapshot Restore] Migrate to new ES client (#95499)
* wip, migrated routes and plugins * refactored all ES error handling to use handleEsError and new isEsError detection * - fixed Jest tests for new es client - updated routes in light of new responses * remove unused import * remove unecessary isEsError check in rest api route handlers * mute all incorrect types from client lib using @ts-expect-error * reordered and clean up imports, removed legacy client code * update legacy test runner * updated use of legacyES Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a9d0b6f478
commit
44a46358c2
|
@ -1,101 +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.sr = components.clientAction.namespaceFactory();
|
||||
const sr = Client.prototype.sr.prototype;
|
||||
|
||||
sr.policies = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/policy',
|
||||
},
|
||||
],
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
sr.policy = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/policy/<%=name%>',
|
||||
req: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
sr.deletePolicy = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/policy/<%=name%>',
|
||||
req: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
sr.executePolicy = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/policy/<%=name%>/_execute',
|
||||
req: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
method: 'PUT',
|
||||
});
|
||||
|
||||
sr.updatePolicy = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/policy/<%=name%>',
|
||||
req: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
method: 'PUT',
|
||||
});
|
||||
|
||||
sr.executeRetention = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/_execute_retention',
|
||||
},
|
||||
],
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
sr.cleanupRepository = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_snapshot/<%=name%>/_cleanup',
|
||||
req: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
|
@ -5,17 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from 'src/core/server';
|
||||
|
||||
// Cloud has its own system for managing SLM policies and we want to make
|
||||
// this clear when Snapshot and Restore is used in a Cloud deployment.
|
||||
// Retrieve the Cloud-managed policies so that UI can switch
|
||||
// logical paths based on this information.
|
||||
export const getManagedPolicyNames = async (callWithInternalUser: any): Promise<string[]> => {
|
||||
export const getManagedPolicyNames = async (
|
||||
clusterClient: ElasticsearchClient
|
||||
): Promise<string[]> => {
|
||||
try {
|
||||
const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', {
|
||||
filterPath: '*.*managed_policies',
|
||||
flatSettings: true,
|
||||
includeDefaults: true,
|
||||
const {
|
||||
body: { persistent, transient, defaults },
|
||||
} = await clusterClient.cluster.getSettings({
|
||||
filter_path: '*.*managed_policies',
|
||||
flat_settings: true,
|
||||
include_defaults: true,
|
||||
});
|
||||
|
||||
const { 'cluster.metadata.managed_policies': managedPolicyNames = [] } = {
|
||||
...defaults,
|
||||
...persistent,
|
||||
|
|
|
@ -5,18 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from 'src/core/server';
|
||||
|
||||
// Cloud has its own system for managing snapshots and we want to make
|
||||
// this clear when Snapshot and Restore is used in a Cloud deployment.
|
||||
// Retrieve the Cloud-managed repository name so that UI can switch
|
||||
// logical paths based on this information.
|
||||
export const getManagedRepositoryName = async (
|
||||
callWithInternalUser: any
|
||||
client: ElasticsearchClient
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', {
|
||||
filterPath: '*.*managed_repository',
|
||||
flatSettings: true,
|
||||
includeDefaults: true,
|
||||
const {
|
||||
body: { persistent, transient, defaults },
|
||||
} = await client.cluster.getSettings({
|
||||
filter_path: '*.*managed_repository',
|
||||
flat_settings: true,
|
||||
include_defaults: true,
|
||||
});
|
||||
const { 'cluster.metadata.managed_repository': managedRepositoryName = undefined } = {
|
||||
...defaults,
|
||||
|
|
|
@ -6,34 +6,20 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
CoreSetup,
|
||||
ILegacyCustomClusterClient,
|
||||
Plugin,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
} from 'kibana/server';
|
||||
import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'kibana/server';
|
||||
|
||||
import { PLUGIN, APP_REQUIRED_CLUSTER_PRIVILEGES } from '../common';
|
||||
import { License } from './services';
|
||||
import { ApiRoutes } from './routes';
|
||||
import { wrapEsError } from './lib';
|
||||
import { isEsError } from './shared_imports';
|
||||
import { elasticsearchJsPlugin } from './client/elasticsearch_sr';
|
||||
import type { Dependencies, SnapshotRestoreRequestHandlerContext } from './types';
|
||||
import { handleEsError } from './shared_imports';
|
||||
import type { Dependencies } from './types';
|
||||
import { SnapshotRestoreConfig } from './config';
|
||||
|
||||
async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
|
||||
const [core] = await getStartServices();
|
||||
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
|
||||
return core.elasticsearch.legacy.createClient('snapshotRestore', esClientConfig);
|
||||
}
|
||||
|
||||
export class SnapshotRestoreServerPlugin implements Plugin<void, void, any, any> {
|
||||
private readonly logger: Logger;
|
||||
private readonly apiRoutes: ApiRoutes;
|
||||
private readonly license: License;
|
||||
private snapshotRestoreESClient?: ILegacyCustomClusterClient;
|
||||
|
||||
constructor(private context: PluginInitializerContext) {
|
||||
const { logger } = this.context;
|
||||
|
@ -52,7 +38,7 @@ export class SnapshotRestoreServerPlugin implements Plugin<void, void, any, any>
|
|||
return;
|
||||
}
|
||||
|
||||
const router = http.createRouter<SnapshotRestoreRequestHandlerContext>();
|
||||
const router = http.createRouter();
|
||||
|
||||
this.license.setup(
|
||||
{
|
||||
|
@ -82,17 +68,6 @@ export class SnapshotRestoreServerPlugin implements Plugin<void, void, any, any>
|
|||
],
|
||||
});
|
||||
|
||||
http.registerRouteHandlerContext<SnapshotRestoreRequestHandlerContext, 'snapshotRestore'>(
|
||||
'snapshotRestore',
|
||||
async (ctx, request) => {
|
||||
this.snapshotRestoreESClient =
|
||||
this.snapshotRestoreESClient ?? (await getCustomEsClient(getStartServices));
|
||||
return {
|
||||
client: this.snapshotRestoreESClient.asScoped(request),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
this.apiRoutes.setup({
|
||||
router,
|
||||
license: this.license,
|
||||
|
@ -102,17 +77,11 @@ export class SnapshotRestoreServerPlugin implements Plugin<void, void, any, any>
|
|||
isSlmEnabled: pluginConfig.slm_ui.enabled,
|
||||
},
|
||||
lib: {
|
||||
isEsError,
|
||||
handleEsError,
|
||||
wrapEsError,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {
|
||||
if (this.snapshotRestoreESClient) {
|
||||
this.snapshotRestoreESClient.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,12 +27,12 @@ export function registerAppRoutes({
|
|||
router,
|
||||
config: { isSecurityEnabled },
|
||||
license,
|
||||
lib: { isEsError },
|
||||
lib: { handleEsError },
|
||||
}: RouteDependencies) {
|
||||
router.get(
|
||||
{ path: addBasePath('privileges'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
const privilegesResult: Privileges = {
|
||||
hasAllPrivileges: true,
|
||||
|
@ -48,42 +48,36 @@ export function registerAppRoutes({
|
|||
}
|
||||
|
||||
try {
|
||||
// Get cluster priviliges
|
||||
const { has_all_requested: hasAllPrivileges, cluster } = await callAsCurrentUser(
|
||||
'transport.request',
|
||||
{
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES],
|
||||
},
|
||||
}
|
||||
);
|
||||
// Get cluster privileges
|
||||
const {
|
||||
body: { has_all_requested: hasAllPrivileges, cluster },
|
||||
} = await clusterClient.asCurrentUser.security.hasPrivileges({
|
||||
body: {
|
||||
cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES],
|
||||
},
|
||||
});
|
||||
|
||||
// Find missing cluster privileges and set overall app privileges
|
||||
privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster);
|
||||
privilegesResult.hasAllPrivileges = hasAllPrivileges;
|
||||
|
||||
// Get all index privileges the user has
|
||||
const { indices } = await callAsCurrentUser('transport.request', {
|
||||
path: '/_security/user/_privileges',
|
||||
method: 'GET',
|
||||
});
|
||||
const {
|
||||
body: { indices },
|
||||
} = await clusterClient.asCurrentUser.security.getUserPrivileges();
|
||||
|
||||
// Check if they have all the required index privileges for at least one index
|
||||
const oneIndexWithAllPrivileges = indices.find(
|
||||
({ privileges }: { privileges: string[] }) => {
|
||||
if (privileges.includes('all')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) =>
|
||||
privileges.includes(privilege)
|
||||
);
|
||||
|
||||
return indexHasAllPrivileges;
|
||||
const oneIndexWithAllPrivileges = indices.find(({ privileges }) => {
|
||||
if (privileges.includes('all')) {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) =>
|
||||
privileges.includes(privilege)
|
||||
);
|
||||
|
||||
return indexHasAllPrivileges;
|
||||
});
|
||||
|
||||
// If they don't, return list of required index privileges
|
||||
if (!oneIndexWithAllPrivileges) {
|
||||
|
@ -92,14 +86,7 @@ export function registerAppRoutes({
|
|||
|
||||
return res.ok({ body: privilegesResult });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -44,12 +44,23 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
isManagedPolicy: false,
|
||||
};
|
||||
|
||||
const router = new RouterMock('snapshotRestore.client');
|
||||
const router = new RouterMock();
|
||||
|
||||
/**
|
||||
* ES APIs used by these endpoints
|
||||
*/
|
||||
const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings');
|
||||
const putClusterSettingsFn = router.getMockApiFn('cluster.putSettings');
|
||||
const getLifecycleFn = router.getMockApiFn('slm.getLifecycle');
|
||||
const putLifecycleFn = router.getMockApiFn('slm.putLifecycle');
|
||||
const executeLifecycleFn = router.getMockApiFn('slm.executeLifecycle');
|
||||
const deleteLifecycleFn = router.getMockApiFn('slm.deleteLifecycle');
|
||||
const resolveIndicesFn = router.getMockApiFn('indices.resolveIndex');
|
||||
|
||||
beforeAll(() => {
|
||||
registerPolicyRoutes({
|
||||
router: router as any,
|
||||
...routeDependencies,
|
||||
router,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -64,7 +75,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
fooPolicy: mockEsPolicy,
|
||||
barPolicy: mockEsPolicy,
|
||||
};
|
||||
router.callAsCurrentUserResponses = [[], mockEsResponse];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: {} });
|
||||
getLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
const expectedResponse = {
|
||||
policies: [
|
||||
{
|
||||
|
@ -84,7 +96,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
|
||||
it('should return empty array if no repositories returned from ES', async () => {
|
||||
const mockEsResponse = {};
|
||||
router.callAsCurrentUserResponses = [[], mockEsResponse];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: {} });
|
||||
getLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
const expectedResponse = { policies: [] };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({
|
||||
body: expectedResponse,
|
||||
|
@ -92,11 +105,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(new Error()), // Get managed policyNames will silently fail
|
||||
jest.fn().mockRejectedValueOnce(new Error()), // Call to 'sr.policies'
|
||||
];
|
||||
|
||||
getClusterSettingsFn.mockRejectedValue(new Error()); // Get managed policyNames should silently fail
|
||||
getLifecycleFn.mockRejectedValue(new Error());
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
@ -116,7 +126,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
[name]: mockEsPolicy,
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [mockEsResponse, {}];
|
||||
getLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
getClusterSettingsFn.mockResolvedValue({ body: {} });
|
||||
|
||||
const expectedResponse = {
|
||||
policy: {
|
||||
|
@ -130,14 +141,20 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
});
|
||||
|
||||
it('should return 404 error if not returned from ES', async () => {
|
||||
router.callAsCurrentUserResponses = [{}, {}];
|
||||
getLifecycleFn.mockRejectedValue({
|
||||
name: 'ResponseError',
|
||||
body: {},
|
||||
statusCode: 404,
|
||||
});
|
||||
getClusterSettingsFn.mockResolvedValue({});
|
||||
|
||||
const response = await router.runRequest(mockRequest);
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
getLifecycleFn.mockRejectedValueOnce(new Error('something unexpected'));
|
||||
getClusterSettingsFn.mockResolvedValueOnce({ body: {} });
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -158,7 +175,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
const mockEsResponse = {
|
||||
snapshot_name: 'foo-policy-snapshot',
|
||||
};
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
executeLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
snapshotName: 'foo-policy-snapshot',
|
||||
|
@ -170,7 +187,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
executeLifecycleFn.mockRejectedValue(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -189,7 +206,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
|
||||
it('should return successful ES responses', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse];
|
||||
deleteLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { itemsDeleted: names, errors: [] };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({
|
||||
|
@ -202,10 +219,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
mockEsError.response = '{}';
|
||||
mockEsError.statusCode = 500;
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
];
|
||||
deleteLifecycleFn.mockRejectedValue(mockEsError);
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [],
|
||||
|
@ -228,10 +242,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
mockEsError.statusCode = 500;
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
mockEsResponse,
|
||||
];
|
||||
deleteLifecycleFn.mockRejectedValueOnce(mockEsError);
|
||||
deleteLifecycleFn.mockResolvedValueOnce({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [names[1]],
|
||||
|
@ -264,7 +276,9 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [{}, mockEsResponse];
|
||||
|
||||
getLifecycleFn.mockResolvedValue({ body: {} });
|
||||
putLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({
|
||||
|
@ -274,14 +288,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
|
||||
it('should return error if policy with the same name already exists', async () => {
|
||||
const mockEsResponse = { [name]: {} };
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
getLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const response = await router.runRequest(mockRequest);
|
||||
expect(response.status).toBe(409);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(new Error())];
|
||||
getLifecycleFn.mockResolvedValue({ body: {} });
|
||||
putLifecycleFn.mockRejectedValue(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -302,14 +317,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse];
|
||||
getLifecycleFn.mockResolvedValue({ body: { [name]: {} } });
|
||||
putLifecycleFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
getLifecycleFn.mockRejectedValue(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -343,7 +359,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
|
||||
resolveIndicesFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
indices: ['fooIndex'],
|
||||
|
@ -358,14 +375,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
aliases: [],
|
||||
data_streams: [],
|
||||
};
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
resolveIndicesFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { indices: [], dataStreams: [] };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
resolveIndicesFn.mockRejectedValueOnce(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -383,14 +400,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
|
|||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
putClusterSettingsFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
putClusterSettingsFn.mockRejectedValue(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PutSnapshotLifecycleRequest } from '@elastic/elasticsearch/api/types';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types';
|
||||
|
@ -17,21 +17,19 @@ import { nameParameterSchema, policySchema } from './validate_schemas';
|
|||
export function registerPolicyRoutes({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, wrapEsError },
|
||||
lib: { wrapEsError, handleEsError },
|
||||
}: RouteDependencies) {
|
||||
// GET all policies
|
||||
router.get(
|
||||
{ path: addBasePath('policies'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
const managedPolicies = await getManagedPolicyNames(callAsCurrentUser);
|
||||
const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser);
|
||||
|
||||
try {
|
||||
// Get policies
|
||||
const policiesByName: {
|
||||
[key: string]: SlmPolicyEs;
|
||||
} = await callAsCurrentUser('sr.policies', {
|
||||
const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({
|
||||
human: true,
|
||||
});
|
||||
|
||||
|
@ -39,19 +37,14 @@ export function registerPolicyRoutes({
|
|||
return res.ok({
|
||||
body: {
|
||||
policies: Object.entries(policiesByName).map(([name, policy]) => {
|
||||
return deserializePolicy(name, policy, managedPolicies);
|
||||
// TODO: Figure out why our {@link SlmPolicyEs} is not compatible with:
|
||||
// import type { SnapshotLifecyclePolicyMetadata } from '@elastic/elasticsearch/api/types';
|
||||
return deserializePolicy(name, policy as SlmPolicyEs, managedPolicies);
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -60,39 +53,25 @@ export function registerPolicyRoutes({
|
|||
router.get(
|
||||
{ path: addBasePath('policy/{name}'), validate: { params: nameParameterSchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
|
||||
try {
|
||||
const policiesByName: {
|
||||
[key: string]: SlmPolicyEs;
|
||||
} = await callAsCurrentUser('sr.policy', {
|
||||
name,
|
||||
const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({
|
||||
policy_id: name,
|
||||
human: true,
|
||||
});
|
||||
|
||||
if (!policiesByName[name]) {
|
||||
// If policy doesn't exist, ES will return 200 with an empty object, so manually throw 404 here
|
||||
return res.notFound({ body: 'Policy not found' });
|
||||
}
|
||||
|
||||
const managedPolicies = await getManagedPolicyNames(callAsCurrentUser);
|
||||
const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser);
|
||||
|
||||
// Deserialize policy
|
||||
return res.ok({
|
||||
body: {
|
||||
policy: deserializePolicy(name, policiesByName[name], managedPolicies),
|
||||
policy: deserializePolicy(name, policiesByName[name] as SlmPolicyEs, managedPolicies),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -101,13 +80,17 @@ export function registerPolicyRoutes({
|
|||
router.post(
|
||||
{ path: addBasePath('policies'), validate: { body: policySchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
const policy = req.body as TypeOf<typeof policySchema>;
|
||||
const { name } = policy;
|
||||
|
||||
try {
|
||||
// Check that policy with the same name doesn't already exist
|
||||
const policyByName = await callAsCurrentUser('sr.policy', { name });
|
||||
const { body: policyByName } = await clusterClient.asCurrentUser.slm.getLifecycle({
|
||||
policy_id: name,
|
||||
});
|
||||
|
||||
if (policyByName[name]) {
|
||||
return res.conflict({ body: 'There is already a policy with that name.' });
|
||||
}
|
||||
|
@ -117,21 +100,15 @@ export function registerPolicyRoutes({
|
|||
|
||||
try {
|
||||
// Otherwise create new policy
|
||||
const response = await callAsCurrentUser('sr.updatePolicy', {
|
||||
name,
|
||||
body: serializePolicy(policy),
|
||||
const response = await clusterClient.asCurrentUser.slm.putLifecycle({
|
||||
policy_id: name,
|
||||
// TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']}
|
||||
body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'],
|
||||
});
|
||||
|
||||
return res.ok({ body: response });
|
||||
return res.ok({ body: response.body });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -143,31 +120,25 @@ export function registerPolicyRoutes({
|
|||
validate: { params: nameParameterSchema, body: policySchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
const policy = req.body as TypeOf<typeof policySchema>;
|
||||
|
||||
try {
|
||||
// Check that policy with the given name exists
|
||||
// If it doesn't exist, 404 will be thrown by ES and will be returned
|
||||
await callAsCurrentUser('sr.policy', { name });
|
||||
await clusterClient.asCurrentUser.slm.getLifecycle({ policy_id: name });
|
||||
|
||||
// Otherwise update policy
|
||||
const response = await callAsCurrentUser('sr.updatePolicy', {
|
||||
name,
|
||||
body: serializePolicy(policy),
|
||||
const response = await clusterClient.asCurrentUser.slm.putLifecycle({
|
||||
policy_id: name,
|
||||
// TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']}
|
||||
body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'],
|
||||
});
|
||||
|
||||
return res.ok({ body: response });
|
||||
return res.ok({ body: response.body });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -176,7 +147,7 @@ export function registerPolicyRoutes({
|
|||
router.delete(
|
||||
{ path: addBasePath('policies/{name}'), validate: { params: nameParameterSchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
const policyNames = name.split(',');
|
||||
|
||||
|
@ -187,7 +158,8 @@ export function registerPolicyRoutes({
|
|||
|
||||
await Promise.all(
|
||||
policyNames.map((policyName) => {
|
||||
return callAsCurrentUser('sr.deletePolicy', { name: policyName })
|
||||
return clusterClient.asCurrentUser.slm
|
||||
.deleteLifecycle({ policy_id: policyName })
|
||||
.then(() => response.itemsDeleted.push(policyName))
|
||||
.catch((e) =>
|
||||
response.errors.push({
|
||||
|
@ -206,23 +178,18 @@ export function registerPolicyRoutes({
|
|||
router.post(
|
||||
{ path: addBasePath('policy/{name}/run'), validate: { params: nameParameterSchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
|
||||
try {
|
||||
const { snapshot_name: snapshotName } = await callAsCurrentUser('sr.executePolicy', {
|
||||
name,
|
||||
const {
|
||||
body: { snapshot_name: snapshotName },
|
||||
} = await clusterClient.asCurrentUser.slm.executeLifecycle({
|
||||
policy_id: name,
|
||||
});
|
||||
return res.ok({ body: { snapshotName } });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -231,19 +198,14 @@ export function registerPolicyRoutes({
|
|||
router.get(
|
||||
{ path: addBasePath('policies/indices'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
try {
|
||||
const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser(
|
||||
'transport.request',
|
||||
{
|
||||
method: 'GET',
|
||||
path: `/_resolve/index/*`,
|
||||
query: {
|
||||
expand_wildcards: 'all',
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await clusterClient.asCurrentUser.indices.resolveIndex({
|
||||
name: '*',
|
||||
expand_wildcards: 'all',
|
||||
});
|
||||
const resolvedIndicesResponse = response.body as ResolveIndexResponseFromES;
|
||||
|
||||
const body: PolicyIndicesResponse = {
|
||||
dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(),
|
||||
|
@ -256,14 +218,7 @@ export function registerPolicyRoutes({
|
|||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -272,18 +227,21 @@ export function registerPolicyRoutes({
|
|||
router.get(
|
||||
{ path: addBasePath('policies/retention_settings'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', {
|
||||
filterPath: '**.slm.retention*',
|
||||
includeDefaults: true,
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const {
|
||||
body: { persistent, transient, defaults },
|
||||
} = await clusterClient.asCurrentUser.cluster.getSettings({
|
||||
filter_path: '**.slm.retention*',
|
||||
include_defaults: true,
|
||||
});
|
||||
const { slm: retentionSettings = undefined } = {
|
||||
const { slm: retentionSettings }: { slm?: { retention_schedule: string } } = {
|
||||
...defaults,
|
||||
...persistent,
|
||||
...transient,
|
||||
};
|
||||
|
||||
const { retention_schedule: retentionSchedule } = retentionSettings;
|
||||
const retentionSchedule =
|
||||
retentionSettings != null ? retentionSettings.retention_schedule : undefined;
|
||||
|
||||
return res.ok({
|
||||
body: { retentionSchedule },
|
||||
|
@ -300,11 +258,11 @@ export function registerPolicyRoutes({
|
|||
validate: { body: retentionSettingsSchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { retentionSchedule } = req.body as TypeOf<typeof retentionSettingsSchema>;
|
||||
|
||||
try {
|
||||
const response = await callAsCurrentUser('cluster.putSettings', {
|
||||
const response = await clusterClient.asCurrentUser.cluster.putSettings({
|
||||
body: {
|
||||
persistent: {
|
||||
slm: {
|
||||
|
@ -314,16 +272,9 @@ export function registerPolicyRoutes({
|
|||
},
|
||||
});
|
||||
|
||||
return res.ok({ body: response });
|
||||
return res.ok({ body: response.body });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -332,9 +283,9 @@ export function registerPolicyRoutes({
|
|||
router.post(
|
||||
{ path: addBasePath('policies/retention'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const response = await callAsCurrentUser('sr.executeRetention');
|
||||
return res.ok({ body: response });
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const response = await clusterClient.asCurrentUser.slm.executeRetention();
|
||||
return res.ok({ body: response.body });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,12 +19,25 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const router = new RouterMock('snapshotRestore.client');
|
||||
const router = new RouterMock();
|
||||
|
||||
/**
|
||||
* ES APIs used by these endpoints
|
||||
*/
|
||||
const clusterSettingsFn = router.getMockApiFn('cluster.getSettings');
|
||||
const createRepoFn = router.getMockApiFn('snapshot.createRepository');
|
||||
const getRepoFn = router.getMockApiFn('snapshot.getRepository');
|
||||
const deleteRepoFn = router.getMockApiFn('snapshot.deleteRepository');
|
||||
const getLifecycleFn = router.getMockApiFn('slm.getLifecycle');
|
||||
const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings');
|
||||
const getSnapshotFn = router.getMockApiFn('snapshot.get');
|
||||
const verifyRepoFn = router.getMockApiFn('snapshot.verifyRepository');
|
||||
const catPluginsFn = router.getMockApiFn('cat.plugins');
|
||||
|
||||
beforeAll(() => {
|
||||
registerRepositoriesRoutes({
|
||||
router: router as any,
|
||||
...routeDependencies,
|
||||
router,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -48,11 +61,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
},
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockRepositoryEsResponse,
|
||||
mockPolicyEsResponse,
|
||||
];
|
||||
clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse });
|
||||
getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
repositories: [
|
||||
|
@ -85,11 +96,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
},
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockRepositoryEsResponse,
|
||||
mockPolicyEsResponse,
|
||||
];
|
||||
clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse });
|
||||
getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
repositories: [],
|
||||
|
@ -103,10 +112,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
jest.fn().mockRejectedValueOnce(new Error()),
|
||||
];
|
||||
clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockRejectedValue(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -128,11 +135,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
[name]: { type: '', settings: {} },
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockEsResponse,
|
||||
{},
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockEsResponse });
|
||||
getSnapshotFn.mockResolvedValue({ body: {} });
|
||||
|
||||
const expectedResponse = {
|
||||
repository: { name, ...mockEsResponse[name] },
|
||||
|
@ -144,7 +149,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
});
|
||||
|
||||
it('should return empty repository object if not returned from ES', async () => {
|
||||
router.callAsCurrentUserResponses = [mockSnapshotGetManagedRepositoryEsResponse, {}, {}];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: {} });
|
||||
getSnapshotFn.mockResolvedValue({ body: {} });
|
||||
|
||||
const expectedResponse = {
|
||||
repository: {},
|
||||
|
@ -167,11 +174,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
],
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockEsResponse,
|
||||
mockEsSnapshotResponse,
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockEsResponse });
|
||||
getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
repository: { name, ...mockEsResponse[name] },
|
||||
|
@ -190,11 +195,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
};
|
||||
const mockEsSnapshotError = jest.fn().mockRejectedValueOnce(new Error('snapshot error'));
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockEsResponse,
|
||||
mockEsSnapshotError,
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockEsResponse });
|
||||
getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotError });
|
||||
|
||||
const expectedResponse = {
|
||||
repository: { name, ...mockEsResponse[name] },
|
||||
|
@ -208,10 +211,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
jest.fn().mockRejectedValueOnce(new Error()),
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
|
||||
getRepoFn.mockRejectedValue(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -230,7 +232,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
|
||||
it('should return repository verification response if returned from ES', async () => {
|
||||
const mockEsResponse = { nodes: {} };
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
verifyRepoFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
verification: { valid: true, response: mockEsResponse },
|
||||
|
@ -241,7 +243,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
|
||||
it('should return repository verification error if returned from ES', async () => {
|
||||
const mockEsResponse = { error: {}, status: 500 };
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(mockEsResponse)];
|
||||
verifyRepoFn.mockRejectedValueOnce(mockEsResponse);
|
||||
|
||||
const expectedResponse = {
|
||||
verification: { valid: false, error: mockEsResponse },
|
||||
|
@ -258,7 +260,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
};
|
||||
|
||||
it('should return default types if no repository plugins returned from ES', async () => {
|
||||
router.callAsCurrentUserResponses = [{}];
|
||||
catPluginsFn.mockResolvedValue({ body: {} });
|
||||
|
||||
const expectedResponse = [...DEFAULT_REPOSITORY_TYPES];
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
|
||||
|
@ -269,7 +271,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
const pluginTypes = Object.entries(REPOSITORY_PLUGINS_MAP).map(([key, value]) => value);
|
||||
|
||||
const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))];
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
catPluginsFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = [...DEFAULT_REPOSITORY_TYPES, ...pluginTypes];
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
|
||||
|
@ -278,7 +280,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
it('should not return non-repository plugins returned from ES', async () => {
|
||||
const pluginNames = ['foo-plugin', 'bar-plugin'];
|
||||
const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))];
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
catPluginsFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = [...DEFAULT_REPOSITORY_TYPES];
|
||||
|
||||
|
@ -286,11 +288,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(new Error('Error getting pluggins')),
|
||||
];
|
||||
catPluginsFn.mockRejectedValueOnce(new Error('Error getting plugins'));
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting pluggins');
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting plugins');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -307,7 +307,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [{}, mockEsResponse];
|
||||
getRepoFn.mockResolvedValue({ body: {} });
|
||||
createRepoFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
|
||||
|
@ -315,15 +316,15 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
});
|
||||
|
||||
it('should return error if repository with the same name already exists', async () => {
|
||||
router.callAsCurrentUserResponses = [{ [name]: {} }];
|
||||
|
||||
getRepoFn.mockResolvedValue({ body: { [name]: {} } });
|
||||
const response = await router.runRequest(mockRequest);
|
||||
expect(response.status).toBe(409);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const error = new Error('Oh no!');
|
||||
router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(error)];
|
||||
getRepoFn.mockResolvedValue({ body: {} });
|
||||
createRepoFn.mockRejectedValue(error);
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
|
@ -344,7 +345,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse];
|
||||
getRepoFn.mockResolvedValue({ body: { [name]: {} } });
|
||||
createRepoFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = mockEsResponse;
|
||||
|
||||
|
@ -352,7 +354,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
getRepoFn.mockRejectedValue(new Error());
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
@ -369,7 +371,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
|
||||
it('should return successful ES responses', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse];
|
||||
deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse });
|
||||
deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = { itemsDeleted: names, errors: [] };
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
|
||||
|
@ -380,10 +383,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
mockEsError.response = '{}';
|
||||
mockEsError.statusCode = 500;
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
];
|
||||
deleteRepoFn.mockRejectedValueOnce(mockEsError);
|
||||
deleteRepoFn.mockRejectedValueOnce(mockEsError);
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [],
|
||||
|
@ -402,11 +403,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => {
|
|||
mockEsError.response = '{}';
|
||||
mockEsError.statusCode = 500;
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const responses = [Promise.reject(mockEsError), Promise.resolve({ body: mockEsResponse })];
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
mockEsResponse,
|
||||
];
|
||||
deleteRepoFn.mockImplementation(() => responses.shift());
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [names[1]],
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import type { SnapshotRepositorySettings } from '@elastic/elasticsearch/api/types';
|
||||
|
||||
import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants';
|
||||
import { Repository, RepositoryType, SlmPolicyEs } from '../../../common/types';
|
||||
import { Repository, RepositoryType } from '../../../common/types';
|
||||
import { RouteDependencies } from '../../types';
|
||||
import { addBasePath } from '../helpers';
|
||||
import { nameParameterSchema, repositorySchema } from './validate_schemas';
|
||||
|
@ -28,21 +29,23 @@ export function registerRepositoriesRoutes({
|
|||
router,
|
||||
license,
|
||||
config: { isCloudEnabled },
|
||||
lib: { isEsError, wrapEsError },
|
||||
lib: { wrapEsError, handleEsError },
|
||||
}: RouteDependencies) {
|
||||
// GET all repositories
|
||||
router.get(
|
||||
{ path: addBasePath('repositories'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const managedRepositoryName = await getManagedRepositoryName(callAsCurrentUser);
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const managedRepositoryName = await getManagedRepositoryName(clusterClient.asCurrentUser);
|
||||
|
||||
let repositoryNames: string[] | undefined;
|
||||
let repositories: Repository[];
|
||||
let managedRepository: ManagedRepository;
|
||||
|
||||
try {
|
||||
const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', {
|
||||
const {
|
||||
body: repositoriesByName,
|
||||
} = await clusterClient.asCurrentUser.snapshot.getRepository({
|
||||
repository: '_all',
|
||||
});
|
||||
repositoryNames = Object.keys(repositoriesByName);
|
||||
|
@ -52,29 +55,20 @@ export function registerRepositoriesRoutes({
|
|||
name,
|
||||
type,
|
||||
settings: deserializeRepositorySettings(settings),
|
||||
};
|
||||
} as Repository;
|
||||
});
|
||||
|
||||
managedRepository = {
|
||||
name: managedRepositoryName,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
|
||||
// If a managed repository, we also need to check if a policy is associated to it
|
||||
if (managedRepositoryName) {
|
||||
try {
|
||||
const policiesByName: {
|
||||
[key: string]: SlmPolicyEs;
|
||||
} = await callAsCurrentUser('sr.policies', {
|
||||
const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({
|
||||
human: true,
|
||||
});
|
||||
|
||||
|
@ -102,45 +96,28 @@ export function registerRepositoriesRoutes({
|
|||
router.get(
|
||||
{ path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
|
||||
const managedRepository = await getManagedRepositoryName(callAsCurrentUser);
|
||||
const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser);
|
||||
|
||||
let repositoryByName: any;
|
||||
|
||||
try {
|
||||
repositoryByName = await callAsCurrentUser('snapshot.getRepository', {
|
||||
({ body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository({
|
||||
repository: name,
|
||||
});
|
||||
}));
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
|
||||
const {
|
||||
responses: snapshotResponses,
|
||||
}: {
|
||||
responses: Array<{
|
||||
repository: string;
|
||||
snapshots: any[];
|
||||
}>;
|
||||
} = await callAsCurrentUser('snapshot.get', {
|
||||
const response = await clusterClient.asCurrentUser.snapshot.get({
|
||||
repository: name,
|
||||
snapshot: '_all',
|
||||
}).catch((e) => ({
|
||||
responses: [
|
||||
{
|
||||
snapshots: null,
|
||||
},
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
// @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250.
|
||||
const { responses: snapshotResponses } = response.body;
|
||||
|
||||
if (repositoryByName[name]) {
|
||||
const { type = '', settings = {} } = repositoryByName[name];
|
||||
|
@ -176,18 +153,20 @@ export function registerRepositoriesRoutes({
|
|||
router.get(
|
||||
{ path: addBasePath('repository_types'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
// In ECE/ESS, do not enable the default types
|
||||
const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES];
|
||||
|
||||
try {
|
||||
// Call with internal user so that the requesting user does not need `monitoring` cluster
|
||||
// privilege just to see list of available repository types
|
||||
const plugins: any[] = await callAsCurrentUser('cat.plugins', { format: 'json' });
|
||||
const { body: plugins } = await clusterClient.asCurrentUser.cat.plugins({ format: 'json' });
|
||||
|
||||
// Filter list of plugins to repository-related ones
|
||||
if (plugins && plugins.length) {
|
||||
const pluginNames: string[] = [...new Set(plugins.map((plugin) => plugin.component))];
|
||||
const pluginNames: string[] = [
|
||||
...new Set(plugins.map((plugin) => plugin.component ?? '')),
|
||||
];
|
||||
pluginNames.forEach((pluginName) => {
|
||||
if (REPOSITORY_PLUGINS_MAP[pluginName]) {
|
||||
types.push(REPOSITORY_PLUGINS_MAP[pluginName]);
|
||||
|
@ -196,14 +175,7 @@ export function registerRepositoriesRoutes({
|
|||
}
|
||||
return res.ok({ body: types });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -215,20 +187,24 @@ export function registerRepositoriesRoutes({
|
|||
validate: { params: nameParameterSchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
|
||||
try {
|
||||
const verificationResults = await callAsCurrentUser('snapshot.verifyRepository', {
|
||||
repository: name,
|
||||
}).catch((e) => ({
|
||||
valid: false,
|
||||
error: e.response ? JSON.parse(e.response) : e,
|
||||
}));
|
||||
const { body: verificationResults } = await clusterClient.asCurrentUser.snapshot
|
||||
.verifyRepository({
|
||||
repository: name,
|
||||
})
|
||||
.catch((e) => ({
|
||||
body: {
|
||||
valid: false,
|
||||
error: e.response ? JSON.parse(e.response) : e,
|
||||
},
|
||||
}));
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
verification: verificationResults.error
|
||||
verification: (verificationResults as { error?: Error }).error
|
||||
? verificationResults
|
||||
: {
|
||||
valid: true,
|
||||
|
@ -237,14 +213,7 @@ export function registerRepositoriesRoutes({
|
|||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -256,20 +225,24 @@ export function registerRepositoriesRoutes({
|
|||
validate: { params: nameParameterSchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
|
||||
try {
|
||||
const cleanupResults = await callAsCurrentUser('sr.cleanupRepository', {
|
||||
name,
|
||||
}).catch((e) => ({
|
||||
cleaned: false,
|
||||
error: e.response ? JSON.parse(e.response) : e,
|
||||
}));
|
||||
const { body: cleanupResults } = await clusterClient.asCurrentUser.snapshot
|
||||
.cleanupRepository({
|
||||
repository: name,
|
||||
})
|
||||
.catch((e) => ({
|
||||
body: {
|
||||
cleaned: false,
|
||||
error: e.response ? JSON.parse(e.response) : e,
|
||||
},
|
||||
}));
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
cleanup: cleanupResults.error
|
||||
cleanup: (cleanupResults as { error?: Error }).error
|
||||
? cleanupResults
|
||||
: {
|
||||
cleaned: true,
|
||||
|
@ -278,14 +251,7 @@ export function registerRepositoriesRoutes({
|
|||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -294,14 +260,16 @@ export function registerRepositoriesRoutes({
|
|||
router.put(
|
||||
{ path: addBasePath('repositories'), validate: { body: repositorySchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name = '', type = '', settings = {} } = req.body as TypeOf<typeof repositorySchema>;
|
||||
|
||||
// Check that repository with the same name doesn't already exist
|
||||
try {
|
||||
const repositoryByName = await callAsCurrentUser('snapshot.getRepository', {
|
||||
repository: name,
|
||||
});
|
||||
const { body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository(
|
||||
{
|
||||
repository: name,
|
||||
}
|
||||
);
|
||||
if (repositoryByName[name]) {
|
||||
return res.conflict({ body: 'There is already a repository with that name.' });
|
||||
}
|
||||
|
@ -311,25 +279,19 @@ export function registerRepositoriesRoutes({
|
|||
|
||||
// Otherwise create new repository
|
||||
try {
|
||||
const response = await callAsCurrentUser('snapshot.createRepository', {
|
||||
const response = await clusterClient.asCurrentUser.snapshot.createRepository({
|
||||
repository: name,
|
||||
body: {
|
||||
type,
|
||||
settings: serializeRepositorySettings(settings),
|
||||
// TODO: Bring {@link RepositorySettings} in line with {@link SnapshotRepositorySettings}
|
||||
settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings,
|
||||
},
|
||||
verify: false,
|
||||
});
|
||||
|
||||
return res.ok({ body: response });
|
||||
return res.ok({ body: response.body });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -341,37 +303,30 @@ export function registerRepositoriesRoutes({
|
|||
validate: { body: repositorySchema, params: nameParameterSchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
const { type = '', settings = {} } = req.body as TypeOf<typeof repositorySchema>;
|
||||
|
||||
try {
|
||||
// Check that repository with the given name exists
|
||||
// If it doesn't exist, 404 will be thrown by ES and will be returned
|
||||
await callAsCurrentUser('snapshot.getRepository', { repository: name });
|
||||
await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name });
|
||||
|
||||
// Otherwise update repository
|
||||
const response = await callAsCurrentUser('snapshot.createRepository', {
|
||||
const response = await clusterClient.asCurrentUser.snapshot.createRepository({
|
||||
repository: name,
|
||||
body: {
|
||||
type,
|
||||
settings: serializeRepositorySettings(settings),
|
||||
settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings,
|
||||
},
|
||||
verify: false,
|
||||
});
|
||||
|
||||
return res.ok({
|
||||
body: response,
|
||||
body: response.body,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -380,7 +335,7 @@ export function registerRepositoriesRoutes({
|
|||
router.delete(
|
||||
{ path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { name } = req.params as TypeOf<typeof nameParameterSchema>;
|
||||
const repositoryNames = name.split(',');
|
||||
|
||||
|
@ -392,7 +347,8 @@ export function registerRepositoriesRoutes({
|
|||
try {
|
||||
await Promise.all(
|
||||
repositoryNames.map((repoName) => {
|
||||
return callAsCurrentUser('snapshot.deleteRepository', { repository: repoName })
|
||||
return clusterClient.asCurrentUser.snapshot
|
||||
.deleteRepository({ repository: repoName })
|
||||
.then(() => response.itemsDeleted.push(repoName))
|
||||
.catch((e) =>
|
||||
response.errors.push({
|
||||
|
@ -405,14 +361,7 @@ export function registerRepositoriesRoutes({
|
|||
|
||||
return res.ok({ body: response });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -17,15 +17,21 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
index: { size: {}, files: {} },
|
||||
};
|
||||
|
||||
const router = new RouterMock('snapshotRestore.client');
|
||||
const router = new RouterMock();
|
||||
|
||||
beforeAll(() => {
|
||||
registerRestoreRoutes({
|
||||
router: router as any,
|
||||
...routeDependencies,
|
||||
router,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ES APIs used by these endpoints
|
||||
*/
|
||||
const indicesRecoveryFn = router.getMockApiFn('indices.recovery');
|
||||
const restoreSnapshotFn = router.getMockApiFn('snapshot.restore');
|
||||
|
||||
describe('Restore snapshot', () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
|
@ -39,7 +45,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
|
||||
it('should return successful response from ES', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
restoreSnapshotFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({
|
||||
body: mockEsResponse,
|
||||
|
@ -47,7 +53,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
restoreSnapshotFn.mockRejectedValue(new Error());
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
@ -76,7 +82,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
},
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = [
|
||||
{
|
||||
|
@ -100,7 +106,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
|
||||
it('should return empty array if no repositories returned from ES', async () => {
|
||||
const mockEsResponse = {};
|
||||
router.callAsCurrentUserResponses = [mockEsResponse];
|
||||
indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse });
|
||||
const expectedResponse: any[] = [];
|
||||
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({
|
||||
|
@ -109,7 +115,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())];
|
||||
indicesRecoveryFn.mockRejectedValue(new Error());
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { RestoreRequest } from '@elastic/elasticsearch/api/types';
|
||||
|
||||
import { SnapshotRestore, SnapshotRestoreShardEs } from '../../../common/types';
|
||||
import { serializeRestoreSettings } from '../../../common/lib';
|
||||
|
@ -14,20 +15,20 @@ import { RouteDependencies } from '../../types';
|
|||
import { addBasePath } from '../helpers';
|
||||
import { restoreSettingsSchema } from './validate_schemas';
|
||||
|
||||
export function registerRestoreRoutes({ router, license, lib: { isEsError } }: RouteDependencies) {
|
||||
export function registerRestoreRoutes({
|
||||
router,
|
||||
license,
|
||||
lib: { handleEsError },
|
||||
}: RouteDependencies) {
|
||||
// GET all snapshot restores
|
||||
router.get(
|
||||
{ path: addBasePath('restores'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
try {
|
||||
const snapshotRestores: SnapshotRestore[] = [];
|
||||
const recoveryByIndexName: {
|
||||
[key: string]: {
|
||||
shards: SnapshotRestoreShardEs[];
|
||||
};
|
||||
} = await callAsCurrentUser('indices.recovery', {
|
||||
const { body: recoveryByIndexName } = await clusterClient.asCurrentUser.indices.recovery({
|
||||
human: true,
|
||||
});
|
||||
|
||||
|
@ -40,7 +41,8 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R
|
|||
.filter((shard) => shard.type === 'SNAPSHOT')
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((shard) => {
|
||||
const deserializedShard = deserializeRestoreShard(shard);
|
||||
// TODO: Bring {@link SnapshotRestoreShardEs} in line with {@link ShardRecovery}
|
||||
const deserializedShard = deserializeRestoreShard(shard as SnapshotRestoreShardEs);
|
||||
const { startTimeInMillis, stopTimeInMillis } = deserializedShard;
|
||||
|
||||
// Set overall latest activity time
|
||||
|
@ -80,14 +82,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R
|
|||
|
||||
return res.ok({ body: snapshotRestores });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -104,27 +99,21 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R
|
|||
validate: { body: restoreSettingsSchema, params: restoreParamsSchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { repository, snapshot } = req.params as TypeOf<typeof restoreParamsSchema>;
|
||||
const restoreSettings = req.body as TypeOf<typeof restoreSettingsSchema>;
|
||||
|
||||
try {
|
||||
const response = await callAsCurrentUser('snapshot.restore', {
|
||||
const response = await clusterClient.asCurrentUser.snapshot.restore({
|
||||
repository,
|
||||
snapshot,
|
||||
body: serializeRestoreSettings(restoreSettings),
|
||||
// TODO: Bring {@link RestoreSettingsEs} in line with {@link RestoreRequest['body']}
|
||||
body: serializeRestoreSettings(restoreSettings) as RestoreRequest['body'],
|
||||
});
|
||||
|
||||
return res.ok({ body: response });
|
||||
return res.ok({ body: response.body });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -29,12 +29,21 @@ const defaultSnapshot = {
|
|||
};
|
||||
|
||||
describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
||||
const router = new RouterMock('snapshotRestore.client');
|
||||
const router = new RouterMock();
|
||||
|
||||
/**
|
||||
* ES APIs used by these endpoints
|
||||
*/
|
||||
const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings');
|
||||
const getLifecycleFn = router.getMockApiFn('slm.getLifecycle');
|
||||
const getRepoFn = router.getMockApiFn('snapshot.getRepository');
|
||||
const getSnapshotFn = router.getMockApiFn('snapshot.get');
|
||||
const deleteSnapshotFn = router.getMockApiFn('snapshot.delete');
|
||||
|
||||
beforeAll(() => {
|
||||
registerSnapshotsRoutes({
|
||||
router: router as any,
|
||||
...routeDependencies,
|
||||
router,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -60,31 +69,29 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
barRepository: {},
|
||||
};
|
||||
|
||||
const mockGetSnapshotsFooResponse = Promise.resolve({
|
||||
const mockGetSnapshotsFooResponse = {
|
||||
responses: [
|
||||
{
|
||||
repository: 'fooRepository',
|
||||
snapshots: [{ snapshot: 'snapshot1' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const mockGetSnapshotsBarResponse = Promise.resolve({
|
||||
const mockGetSnapshotsBarResponse = {
|
||||
responses: [
|
||||
{
|
||||
repository: 'barRepository',
|
||||
snapshots: [{ snapshot: 'snapshot2' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockSnapshotGetPolicyEsResponse,
|
||||
mockSnapshotGetRepositoryEsResponse,
|
||||
mockGetSnapshotsFooResponse,
|
||||
mockGetSnapshotsBarResponse,
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse });
|
||||
getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsFooResponse });
|
||||
getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsBarResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
errors: {},
|
||||
|
@ -120,11 +127,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
const mockSnapshotGetPolicyEsResponse = {};
|
||||
const mockSnapshotGetRepositoryEsResponse = {};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockSnapshotGetPolicyEsResponse,
|
||||
mockSnapshotGetRepositoryEsResponse,
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse });
|
||||
getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
errors: [],
|
||||
|
@ -138,11 +143,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
});
|
||||
|
||||
test('throws if ES error', async () => {
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(new Error('Error getting managed repository')),
|
||||
jest.fn().mockRejectedValueOnce(new Error('Error getting policies')),
|
||||
jest.fn().mockRejectedValueOnce(new Error('Error getting repository')),
|
||||
];
|
||||
getClusterSettingsFn.mockRejectedValueOnce(new Error());
|
||||
getLifecycleFn.mockRejectedValueOnce(new Error());
|
||||
getRepoFn.mockRejectedValueOnce(new Error());
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
});
|
||||
|
@ -177,10 +180,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
],
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockSnapshotGetEsResponse,
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
...defaultSnapshot,
|
||||
|
@ -215,12 +216,13 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
],
|
||||
};
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
mockSnapshotGetManagedRepositoryEsResponse,
|
||||
mockSnapshotGetEsResponse,
|
||||
];
|
||||
getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse });
|
||||
getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse });
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError();
|
||||
await expect(router.runRequest(mockRequest)).resolves.toEqual({
|
||||
body: 'Snapshot not found',
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -243,7 +245,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
it('should return successful ES responses', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
|
||||
router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse];
|
||||
deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse });
|
||||
deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [
|
||||
|
@ -261,10 +264,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
mockEsError.response = '{}';
|
||||
mockEsError.statusCode = 500;
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
];
|
||||
deleteSnapshotFn.mockRejectedValueOnce(mockEsError);
|
||||
deleteSnapshotFn.mockRejectedValueOnce(mockEsError);
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [],
|
||||
|
@ -289,10 +290,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
mockEsError.statusCode = 500;
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
|
||||
router.callAsCurrentUserResponses = [
|
||||
jest.fn().mockRejectedValueOnce(mockEsError),
|
||||
mockEsResponse,
|
||||
];
|
||||
deleteSnapshotFn.mockRejectedValueOnce(mockEsError);
|
||||
deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse });
|
||||
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [{ snapshot: 'snapshot-2', repository: 'barRepository' }],
|
||||
|
|
|
@ -6,31 +6,31 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { RouteDependencies } from '../../types';
|
||||
import { addBasePath } from '../helpers';
|
||||
import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types';
|
||||
import type { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types';
|
||||
import { deserializeSnapshotDetails } from '../../../common/lib';
|
||||
import type { RouteDependencies } from '../../types';
|
||||
import { getManagedRepositoryName } from '../../lib';
|
||||
import { addBasePath } from '../helpers';
|
||||
|
||||
export function registerSnapshotsRoutes({
|
||||
router,
|
||||
license,
|
||||
lib: { isEsError, wrapEsError },
|
||||
lib: { wrapEsError, handleEsError },
|
||||
}: RouteDependencies) {
|
||||
// GET all snapshots
|
||||
router.get(
|
||||
{ path: addBasePath('snapshots'), validate: false },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
const managedRepository = await getManagedRepositoryName(callAsCurrentUser);
|
||||
const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser);
|
||||
|
||||
let policies: string[] = [];
|
||||
|
||||
// Attempt to retrieve policies
|
||||
// This could fail if user doesn't have access to read SLM policies
|
||||
try {
|
||||
const policiesByName = await callAsCurrentUser('sr.policies');
|
||||
const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle();
|
||||
policies = Object.keys(policiesByName);
|
||||
} catch (e) {
|
||||
// Silently swallow error as policy names aren't required in UI
|
||||
|
@ -44,7 +44,9 @@ export function registerSnapshotsRoutes({
|
|||
let repositoryNames: string[];
|
||||
|
||||
try {
|
||||
const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', {
|
||||
const {
|
||||
body: repositoriesByName,
|
||||
} = await clusterClient.asCurrentUser.snapshot.getRepository({
|
||||
repository: '_all',
|
||||
});
|
||||
repositoryNames = Object.keys(repositoriesByName);
|
||||
|
@ -55,13 +57,7 @@ export function registerSnapshotsRoutes({
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
|
||||
const snapshots: SnapshotDetails[] = [];
|
||||
|
@ -71,23 +67,27 @@ export function registerSnapshotsRoutes({
|
|||
const fetchSnapshotsForRepository = async (repository: string) => {
|
||||
try {
|
||||
// If any of these repositories 504 they will cost the request significant time.
|
||||
const {
|
||||
responses: fetchedResponses,
|
||||
}: {
|
||||
responses: Array<{
|
||||
repository: 'string';
|
||||
snapshots: SnapshotDetailsEs[];
|
||||
}>;
|
||||
} = await callAsCurrentUser('snapshot.get', {
|
||||
const response = await clusterClient.asCurrentUser.snapshot.get({
|
||||
repository,
|
||||
snapshot: '_all',
|
||||
ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable.
|
||||
});
|
||||
|
||||
// @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250.
|
||||
const { responses: fetchedResponses } = response.body;
|
||||
|
||||
// Decorate each snapshot with the repository with which it's associated.
|
||||
// @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
|
||||
fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => {
|
||||
// @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
|
||||
fetchedSnapshots.forEach((snapshot) => {
|
||||
snapshots.push(deserializeSnapshotDetails(repository, snapshot, managedRepository));
|
||||
snapshots.push(
|
||||
deserializeSnapshotDetails(
|
||||
repository,
|
||||
snapshot as SnapshotDetailsEs,
|
||||
managedRepository
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -124,28 +124,27 @@ export function registerSnapshotsRoutes({
|
|||
validate: { params: getOneParamsSchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
const { repository, snapshot } = req.params as TypeOf<typeof getOneParamsSchema>;
|
||||
const managedRepository = await getManagedRepositoryName(callAsCurrentUser);
|
||||
const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser);
|
||||
|
||||
try {
|
||||
const {
|
||||
responses: snapshotsResponse,
|
||||
}: {
|
||||
responses: Array<{
|
||||
repository: string;
|
||||
snapshots: SnapshotDetailsEs[];
|
||||
error?: any;
|
||||
}>;
|
||||
} = await callAsCurrentUser('snapshot.get', {
|
||||
const response = await clusterClient.asCurrentUser.snapshot.get({
|
||||
repository,
|
||||
snapshot: '_all',
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
|
||||
// @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250.
|
||||
const { responses: snapshotsResponse } = response.body;
|
||||
|
||||
const snapshotsList =
|
||||
snapshotsResponse && snapshotsResponse[0] && snapshotsResponse[0].snapshots;
|
||||
if (!snapshotsList || snapshotsList.length === 0) {
|
||||
return res.notFound({ body: 'Snapshot not found' });
|
||||
}
|
||||
const selectedSnapshot = snapshotsList.find(
|
||||
// @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
|
||||
({ snapshot: snapshotName }) => snapshot === snapshotName
|
||||
) as SnapshotDetailsEs;
|
||||
|
||||
|
@ -155,10 +154,12 @@ export function registerSnapshotsRoutes({
|
|||
}
|
||||
|
||||
const successfulSnapshots = snapshotsList
|
||||
// @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
|
||||
.filter(({ state }) => state === 'SUCCESS')
|
||||
// @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
|
||||
.sort((a, b) => {
|
||||
return +new Date(b.end_time) - +new Date(a.end_time);
|
||||
});
|
||||
return +new Date(b.end_time!) - +new Date(a.end_time!);
|
||||
}) as SnapshotDetailsEs[];
|
||||
|
||||
return res.ok({
|
||||
body: deserializeSnapshotDetails(
|
||||
|
@ -169,14 +170,7 @@ export function registerSnapshotsRoutes({
|
|||
),
|
||||
});
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -192,7 +186,7 @@ export function registerSnapshotsRoutes({
|
|||
router.post(
|
||||
{ path: addBasePath('snapshots/bulk_delete'), validate: { body: deleteSchema } },
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
|
||||
const { client: clusterClient } = ctx.core.elasticsearch;
|
||||
|
||||
const response: {
|
||||
itemsDeleted: Array<{ snapshot: string; repository: string }>;
|
||||
|
@ -210,7 +204,8 @@ export function registerSnapshotsRoutes({
|
|||
for (let i = 0; i < snapshots.length; i++) {
|
||||
const { snapshot, repository } = snapshots[i];
|
||||
|
||||
await callAsCurrentUser('snapshot.delete', { snapshot, repository })
|
||||
await clusterClient.asCurrentUser.snapshot
|
||||
.delete({ snapshot, repository })
|
||||
.then(() => response.itemsDeleted.push({ snapshot, repository }))
|
||||
.catch((e) =>
|
||||
response.errors.push({
|
||||
|
@ -222,14 +217,7 @@ export function registerSnapshotsRoutes({
|
|||
|
||||
return res.ok({ body: response });
|
||||
} catch (e) {
|
||||
if (isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
throw e;
|
||||
return handleEsError({ error: e, response: res });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -6,11 +6,15 @@
|
|||
*/
|
||||
|
||||
import { Logger } from 'src/core/server';
|
||||
import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'kibana/server';
|
||||
import type {
|
||||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
RequestHandler,
|
||||
RequestHandlerContext,
|
||||
} from 'kibana/server';
|
||||
|
||||
import { LicensingPluginSetup } from '../../../licensing/server';
|
||||
import { LicenseType } from '../../../licensing/common/types';
|
||||
import type { SnapshotRestoreRequestHandlerContext } from '../types';
|
||||
|
||||
export interface LicenseStatus {
|
||||
isValid: boolean;
|
||||
|
@ -51,13 +55,11 @@ export class License {
|
|||
});
|
||||
}
|
||||
|
||||
guardApiRoute<P, Q, B, Context extends SnapshotRestoreRequestHandlerContext>(
|
||||
handler: RequestHandler<P, Q, B, Context>
|
||||
) {
|
||||
guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B>) {
|
||||
const license = this;
|
||||
|
||||
return function licenseCheck(
|
||||
ctx: Context,
|
||||
ctx: RequestHandlerContext,
|
||||
request: KibanaRequest<P, Q, B>,
|
||||
response: KibanaResponseFactory
|
||||
) {
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
|
||||
export { handleEsError } from '../../../../src/plugins/es_ui_shared/server';
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
|
||||
import { License } from '../../services';
|
||||
import { handleEsError } from '../../shared_imports';
|
||||
import { wrapEsError } from '../../lib';
|
||||
import { isEsError } from '../../shared_imports';
|
||||
import type { RouteDependencies } from '../../types';
|
||||
|
||||
const license = new License();
|
||||
license.getStatus = jest.fn().mockReturnValue({ isValid: true });
|
||||
|
||||
export const routeDependencies = {
|
||||
export const routeDependencies: Omit<RouteDependencies, 'router'> = {
|
||||
license,
|
||||
config: {
|
||||
isSecurityEnabled: jest.fn().mockReturnValue(true),
|
||||
|
@ -20,7 +21,7 @@ export const routeDependencies = {
|
|||
isSlmEnabled: true,
|
||||
},
|
||||
lib: {
|
||||
isEsError,
|
||||
wrapEsError,
|
||||
handleEsError,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import type { IRouter } from 'src/core/server';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks';
|
||||
|
||||
type RequestHandler = (...params: any[]) => any;
|
||||
|
||||
|
@ -48,7 +51,7 @@ export interface RequestMock {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class RouterMock {
|
||||
export class RouterMock implements IRouter {
|
||||
/**
|
||||
* Cache to keep a reference to all the request handler defined on the router for each HTTP method and path
|
||||
*/
|
||||
|
@ -60,15 +63,13 @@ export class RouterMock {
|
|||
patch: {},
|
||||
};
|
||||
|
||||
private _callAsCurrentUserCallCount = 0;
|
||||
private _callAsCurrentUserResponses: any[] = [];
|
||||
private contextMock = {};
|
||||
public contextMock = {
|
||||
core: { elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient() } },
|
||||
};
|
||||
|
||||
constructor(pathToESclient = 'core.elasticsearch.dataClient') {
|
||||
set(this.contextMock, pathToESclient, {
|
||||
callAsCurrentUser: this.callAsCurrentUser.bind(this),
|
||||
});
|
||||
}
|
||||
getRoutes = jest.fn();
|
||||
handleLegacyErrors = jest.fn();
|
||||
routerPath = '';
|
||||
|
||||
get({ path }: { path: string }, handler: RequestHandler) {
|
||||
this.cacheHandlers.get[path] = handler;
|
||||
|
@ -90,17 +91,8 @@ export class RouterMock {
|
|||
this.cacheHandlers.patch[path] = handler;
|
||||
}
|
||||
|
||||
private callAsCurrentUser() {
|
||||
const index = this._callAsCurrentUserCallCount;
|
||||
this._callAsCurrentUserCallCount += 1;
|
||||
const response = this._callAsCurrentUserResponses[index];
|
||||
|
||||
return typeof response === 'function' ? Promise.resolve(response()) : Promise.resolve(response);
|
||||
}
|
||||
|
||||
public set callAsCurrentUserResponses(responses: any[]) {
|
||||
this._callAsCurrentUserCallCount = 0;
|
||||
this._callAsCurrentUserResponses = responses;
|
||||
getMockApiFn(path: string): jest.Mock {
|
||||
return get(this.contextMock.core.elasticsearch.client.asCurrentUser, path);
|
||||
}
|
||||
|
||||
runRequest({ method, path, ...mockRequest }: RequestMock) {
|
||||
|
|
|
@ -5,19 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
LegacyScopedClusterClient,
|
||||
ILegacyScopedClusterClient,
|
||||
IRouter,
|
||||
RequestHandlerContext,
|
||||
} from 'src/core/server';
|
||||
import type { IRouter, RequestHandlerContext, IScopedClusterClient } from 'src/core/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { CloudSetup } from '../../cloud/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { License } from './services';
|
||||
import { wrapEsError } from './lib';
|
||||
import { isEsError } from './shared_imports';
|
||||
import { handleEsError } from './shared_imports';
|
||||
|
||||
export interface Dependencies {
|
||||
licensing: LicensingPluginSetup;
|
||||
|
@ -27,7 +22,7 @@ export interface Dependencies {
|
|||
}
|
||||
|
||||
export interface RouteDependencies {
|
||||
router: SnapshotRestoreRouter;
|
||||
router: IRouter;
|
||||
license: License;
|
||||
config: {
|
||||
isSlmEnabled: boolean;
|
||||
|
@ -35,8 +30,8 @@ export interface RouteDependencies {
|
|||
isCloudEnabled: boolean;
|
||||
};
|
||||
lib: {
|
||||
isEsError: typeof isEsError;
|
||||
wrapEsError: typeof wrapEsError;
|
||||
handleEsError: typeof handleEsError;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -56,13 +51,13 @@ export interface ResolveIndexResponseFromES {
|
|||
data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>;
|
||||
}
|
||||
|
||||
export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];
|
||||
export type CallAsCurrentUser = IScopedClusterClient['asCurrentUser'];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface SnapshotRestoreContext {
|
||||
client: ILegacyScopedClusterClient;
|
||||
client: IScopedClusterClient;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,8 +66,3 @@ export interface SnapshotRestoreContext {
|
|||
export interface SnapshotRestoreRequestHandlerContext extends RequestHandlerContext {
|
||||
snapshotRestore: SnapshotRestoreContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type SnapshotRestoreRouter = IRouter<SnapshotRestoreRequestHandlerContext>;
|
||||
|
|
|
@ -14,7 +14,7 @@ interface SlmPolicy {
|
|||
repository: string;
|
||||
isManagedPolicy: boolean;
|
||||
config?: {
|
||||
indices?: string | string[];
|
||||
indices: string | string[];
|
||||
ignoreUnavailable?: boolean;
|
||||
includeGlobalState?: boolean;
|
||||
partial?: boolean;
|
||||
|
@ -36,19 +36,21 @@ interface SlmPolicy {
|
|||
export const registerEsHelpers = (getService: FtrProviderContext['getService']) => {
|
||||
let policiesCreated: string[] = [];
|
||||
|
||||
const es = getService('legacyEs');
|
||||
const es = getService('es');
|
||||
|
||||
const createRepository = (repoName: string) => {
|
||||
return es.snapshot.createRepository({
|
||||
repository: repoName,
|
||||
body: {
|
||||
type: 'fs',
|
||||
settings: {
|
||||
location: '/tmp/',
|
||||
return es.snapshot
|
||||
.createRepository({
|
||||
repository: repoName,
|
||||
body: {
|
||||
type: 'fs',
|
||||
settings: {
|
||||
location: '/tmp/',
|
||||
},
|
||||
},
|
||||
},
|
||||
verify: false,
|
||||
});
|
||||
verify: false,
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
};
|
||||
|
||||
const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => {
|
||||
|
@ -56,20 +58,27 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService'])
|
|||
policiesCreated.push(policy.name);
|
||||
}
|
||||
|
||||
return es.sr.updatePolicy({
|
||||
name: policy.name,
|
||||
body: policy,
|
||||
});
|
||||
return es.slm
|
||||
.putLifecycle({
|
||||
policy_id: policy.name,
|
||||
// TODO: bring {@link SlmPolicy} in line with {@link PutSnapshotLifecycleRequest['body']}
|
||||
// @ts-expect-error
|
||||
body: policy,
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
};
|
||||
|
||||
const getPolicy = (policyName: string) => {
|
||||
return es.sr.policy({
|
||||
name: policyName,
|
||||
human: true,
|
||||
});
|
||||
return es.slm
|
||||
.getLifecycle({
|
||||
policy_id: policyName,
|
||||
human: true,
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
};
|
||||
|
||||
const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName });
|
||||
const deletePolicy = (policyName: string) =>
|
||||
es.slm.deleteLifecycle({ policy_id: policyName }).then(({ body }) => body);
|
||||
|
||||
const cleanupPolicies = () =>
|
||||
Promise.all(policiesCreated.map(deletePolicy))
|
||||
|
|
|
@ -10,7 +10,6 @@ import { format as formatUrl } from 'url';
|
|||
import * as legacyElasticsearch from 'elasticsearch';
|
||||
|
||||
import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch';
|
||||
import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config';
|
||||
|
||||
|
@ -21,6 +20,6 @@ export function LegacyEsProvider({ getService }) {
|
|||
apiVersion: DEFAULT_API_VERSION,
|
||||
host: formatUrl(config.get('servers.elasticsearch')),
|
||||
requestTimeout: config.get('timeouts.esRequestTimeout'),
|
||||
plugins: [indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin],
|
||||
plugins: [indexManagementEsClientPlugin],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common', 'snapshotRestore']);
|
||||
const log = getService('log');
|
||||
const es = getService('legacyEs');
|
||||
const es = getService('es');
|
||||
|
||||
describe('Home page', function () {
|
||||
before(async () => {
|
||||
|
|
Loading…
Reference in a new issue