[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:
Jean-Louis Leysens 2021-03-31 11:55:41 +02:00 committed by GitHub
parent a9d0b6f478
commit 44a46358c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 477 additions and 720 deletions

View file

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

View file

@ -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,

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

@ -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]],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
) {

View file

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

View file

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

View file

@ -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) {

View file

@ -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>;

View file

@ -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))

View file

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

View file

@ -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 () => {